ScreenshotNeo

BlogHow-to

Fix Playwright Full-Page Screenshots That Cut Off the Bottom

When Playwright misses the bottom of a full-page screenshot, check document height, nested scroll containers, and content that loads only after scrolling.

By the ScreenshotNeo team4 October 202610 min read

page.screenshot({ fullPage: true }) captures the full scrollable document, not every independently scrolling element and not content your page has yet to render. If the bottom is missing, first check which element owns the scroll height, then determine whether the missing content exists yet. Scroll and wait for lazy or scroll-triggered content; capture a nested scroller separately; inspect layout constraints such as height: 100vh and overflow. A longer timeout alone does not fix the wrong scroll surface or content that was never loaded.

Playwright defines fullPage as capturing the full scrollable page, rather than just the viewport. See the Playwright screenshots guide and Page.screenshot API.

1. Confirm the screenshot call and inspect the output

Await the screenshot promise, explicitly set fullPage: true, and make sure you opened the newly written file. The option defaults to false. This minimal Node.js example uses Playwright’s library API:

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
    await page.goto('https://example.com', { waitUntil: 'load' });
    await page.screenshot({ path: 'page.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

Install Playwright and a browser in your project before running the script; the project’s standard setup is documented in the Playwright getting started guide. A successful screenshot call only proves that an image was written. It does not prove that lazy content, nested scrollers, or application data are present in it.

2. Diagnose where the missing height lives

Inspect the document and likely scrolling elements after navigation. Compare document.documentElement.scrollHeight, document.body.scrollHeight, and the viewport height. Then check elements with overflow: auto or overflow: scroll whose own scroll height exceeds their client height.

const dimensions = await page.evaluate(() => {
  const doc = document.documentElement;
  const body = document.body;
  const scrollers = [...document.querySelectorAll('*')]
    .filter(el => {
      const style = getComputedStyle(el);
      return /(auto|scroll)/.test(style.overflowY) &&
        el.scrollHeight > el.clientHeight + 1;
    })
    .slice(0, 20)
    .map(el => ({
      tag: el.tagName,
      id: el.id,
      className: typeof el.className === 'string' ? el.className : '',
      clientHeight: el.clientHeight,
      scrollHeight: el.scrollHeight,
      overflowY: getComputedStyle(el).overflowY,
    }));

  return {
    viewportHeight: innerHeight,
    documentElement: { clientHeight: doc.clientHeight, scrollHeight: doc.scrollHeight },
    body: { clientHeight: body.clientHeight, scrollHeight: body.scrollHeight },
    scrollers,
  };
});
console.dir(dimensions, { depth: null });

This is a diagnostic snapshot, not a universal detector: pages can use shadow DOM, iframes, custom scrolling, or virtualized lists. Inspect the actual page and the element the user scrolls. Playwright’s documented full-page behavior concerns the document. A nested scroll container does not become taller just because fullPage is enabled. The explanation in Playwright issue #12962 describes this as expected behavior.

Choose the remedy from the diagnosis

What you find What it means What to do
Document scroll height exceeds viewport; content is already rendered The document has additional height to capture. Use fullPage: true; confirm the saved file is the current output.
A nested element has the larger scroll height The user scrolls an inner container while the document itself stays short. Capture that element with a locator screenshot, or deliberately scroll/capture its contents using a page-specific strategy.
Document is tall, but images or sections are absent Content may depend on scrolling, an observer, a lazy load, or application data. Trigger the content, await a meaningful condition, and capture after it is present.
Document height is close to the viewport despite expected content CSS or application layout may constrain the document. Inspect computed height and overflow rules, including 100vh; adjust the viewport or test-specific styling only if it matches the intended page.

3. Load lazy and scroll-triggered content before capture

A full-page image can be tall while still missing images or sections. Rendering off-screen content for a screenshot does not necessarily perform the real scrolling that triggers IntersectionObserver, loading="lazy", scroll-triggered reveals, or virtualized list updates. A Playwright issue proposes scrolling to load such content; that proposal is not an API option or guarantee. See issue #40941.

For ordinary lazy images and scroll-triggered sections, scroll through the document in increments and wait for images to finish loading before capturing. The following is a reusable starting point, not a guarantee for every framework or virtualized page:

async function scrollDocumentToLoad(page) {
  await page.evaluate(async () => {
    const step = Math.max(200, Math.floor(innerHeight * 0.8));
    let previousHeight = 0;
    let stablePasses = 0;

    for (let i = 0; i < 100; i++) {
      const height = document.documentElement.scrollHeight;
      if (height === previousHeight) stablePasses++;
      else stablePasses = 0;
      if (stablePasses >= 2) break;
      previousHeight = height;
      scrollBy(0, step);
      await new Promise(resolve => setTimeout(resolve, 150));
    }
    scrollTo(0, 0);
  });

  // Wait for currently present images; a page-specific selector or app signal
  // is stronger when the page has known asynchronous content.
  await page.waitForFunction(() =>
    [...document.images].every(img => img.complete)
  );
}

await page.goto('https://example.com', { waitUntil: 'load' });
await scrollDocumentToLoad(page);
await page.screenshot({ path: 'page.png', fullPage: true });

Adapt the stopping condition for pages that append content indefinitely: a fixed iteration cap prevents an endless loop, but may stop before the true end. For a known page, wait for its final section, a specific image’s naturalWidth > 0, or an application-defined ready state. img.complete can also be true for a failed image, so check naturalWidth when successful loading matters. A virtualized list may remove earlier rows as new ones appear; one full-page screenshot cannot preserve rows that the application does not keep in the DOM. Use a page-specific approach, such as collecting data or capturing sections as they are rendered.

4. Capture a nested scroll container

If the missing bottom belongs to a panel, feed, table, or modal with its own scrollbar, fullPage will not expand it. For a container whose complete contents are rendered in the DOM, capture its locator directly:

const panel = page.locator('.scroll-panel');
await panel.screenshot({ path: 'panel.png' });

A locator screenshot captures the element’s rendered box; it does not automatically turn an internally scrollable element into a tall image of every off-screen child. If the content is clipped by the element’s height, use a deliberate strategy: temporarily remove the height/overflow constraint with test-only CSS when that preserves the intended layout, or scroll the container and capture overlapping sections. For example, section captures can be stitched after ensuring every segment represents the same viewport width and page state. Avoid changing production styles blindly: sticky headers, fixed elements, and scroll-linked effects can change when the container expands.

const panel = page.locator('.scroll-panel');
const panelInfo = await panel.evaluate(el => ({
  clientHeight: el.clientHeight,
  scrollHeight: el.scrollHeight,
  scrollTop: el.scrollTop,
}));
console.log(panelInfo);

// Example: scroll the actual container to its end before capturing its final state.
await panel.evaluate(el => { el.scrollTop = el.scrollHeight; });
await page.waitForTimeout(100); // Replace with a content-ready condition if available.
await panel.screenshot({ path: 'panel-at-bottom.png' });

The final example captures the panel at its bottom position, not all of its content in one image. Choose based on whether the goal is a visual of the scrolled state or one tall image containing the entire panel.

5. Check layout constraints and viewport assumptions

Look at the computed styles of the document and suspected wrappers. Fixed heights, height: 100vh, max-height, and overflow: hidden can prevent the document from growing even when an inner section has more content. A larger viewport can be a useful diagnostic when a particular layout changes behavior with viewport size; it is not a general fix. The suggestion in issue #28027 was tied to a specific report.

const layout = await page.evaluate(() => {
  const selectors = ['html', 'body', 'main', '.app', '.content'];
  return selectors.map(selector => {
    const el = document.querySelector(selector);
    if (!el) return { selector, found: false };
    const s = getComputedStyle(el);
    return {
      selector,
      found: true,
      height: s.height,
      minHeight: s.minHeight,
      maxHeight: s.maxHeight,
      overflow: s.overflow,
      overflowY: s.overflowY,
      clientHeight: el.clientHeight,
      scrollHeight: el.scrollHeight,
    };
  });
});
console.table(layout);

If the capture is for a test and the site’s intended content should flow naturally, a screenshot-only stylesheet can remove a known constraint. Playwright’s screenshot API supports a stylesheet option; scope changes narrowly and verify that the altered layout still represents the page you mean to document.

await page.screenshot({
  path: 'page.png',
  fullPage: true,
  style: `
    .known-constrained-wrapper {
      height: auto !important;
      max-height: none !important;
      overflow: visible !important;
    }
  `,
});

6. Screenshot options: what they can and cannot fix

Use screenshot options to control output and presentation, not as substitutes for loading the page. Check the current API reference for the complete, version-specific option set.

Option or behavior Useful for Does not do
fullPage: true Capturing the document’s full scrollable extent. Expanding nested scrollers or forcing lazy content to load.
animations: 'disabled' Reducing visual variation from animations during capture. Waiting for data or triggering scroll observers.
scale: 'css' or 'device' Choosing CSS-pixel or device-pixel output scale. Changing which content has rendered.
clip Capturing a specified rectangle. Creating a full-page capture by itself.
style Applying capture-only CSS, such as hiding a known obstruction or adjusting a diagnosed constraint. Repairing page logic or loading missing data.
timeout Allowing a slow screenshot operation more time before it fails. Fixing a wrong scroll surface or missing content.

Regular screenshots allow animations by default. Screenshot assertions have their own defaults, so set relevant options explicitly when comparing captures. For stable visual checks, also control viewport, device scale factor, page state, fonts, data, and animation behavior.

7. A practical debugging sequence

  1. Save a fresh image to a new path and confirm the capture promise is awaited.
  2. Log document and viewport dimensions; inspect likely scroll containers and computed overflow.
  3. Check whether missing content exists in the DOM and whether its images loaded.
  4. Scroll the page or the actual nested container to trigger page-specific loading, then wait for a concrete condition.
  5. If the document is artificially constrained, adjust the diagnosed CSS or viewport and explain the change in the test.
  6. Capture again, then inspect both image dimensions and the visible bottom edge.

8. Troubleshooting common symptoms

Symptom Likely cause Fix
Screenshot ends at the viewport bottom fullPage omitted, false, or the wrong/stale file is open. Set fullPage: true, await it, write to a fresh path, and verify that file.
Screenshot height is short although the UI has a scrollbar The scrollbar belongs to a nested element. Measure that element’s scrollHeight; capture or scroll the container intentionally.
Tall screenshot has blank image slots or missing sections Lazy loading, an observer, scroll-triggered rendering, failed requests, or app data not ready. Scroll in steps; wait for a meaningful page-specific condition; check image naturalWidth and application state.
Content appears only after manually scrolling Off-screen rendering did not trigger the page’s scroll-dependent behavior. Perform real scrolling before capture and wait after each relevant trigger.
Document reports viewport-sized height A fixed-height wrapper, 100vh, maximum height, or overflow rule constrains it. Inspect computed styles; test a larger viewport or narrow capture-only CSS change if appropriate.
Capture waits a long time and still misses the bottom Time does not trigger the missing behavior; the page may also be waiting on never-ending network activity. Use a targeted readiness condition and the correct scroll strategy rather than only increasing timeout.
Content disappears while scrolling a long feed A virtualized list replaces off-screen items in the DOM. Capture sections during scrolling or use the application’s data/API for a complete record; one full-page render may not contain all rows simultaneously.
Expanded capture looks unlike the site Removing overflow or fixed dimensions changed sticky, fixed, or responsive layout behavior. Keep the original layout and capture segments, or revise only the specific test fixture/style necessary.

9. Performance and reliability notes

Full-page screenshots can require more rendering and image memory as page height grows. Scrolling to trigger content adds time and may cause additional network requests. Keep a finite scroll limit for pages that append content indefinitely; use targeted readiness checks instead of arbitrary long sleeps wherever the page exposes a reliable signal. A page that never becomes network-idle can make a network-idle wait unreliable, so prefer a selector or application state that represents the content you need.

For repeatable results, fix the viewport and device scale, wait for fonts and the specific content under test, disable or finish animations as appropriate, and avoid capturing while the app is still changing. Very long pages and virtualized feeds may be more reliable as multiple captures than as one enormous image. These are engineering tradeoffs; no single timeout or viewport value guarantees success across sites.

10. Or skip the browser setup

If you need a clean page capture without installing and maintaining browser automation, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for parameters and configuration.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card.

FAQ

Does fullPage: true scroll the page for me?

It captures the document’s full scrollable extent; do not assume it will trigger every page behavior that requires actual scrolling.

Is there a Playwright loadLazyContent screenshot option?

The cited issue describes a proposal, not a supported API guarantee. Use explicit page preparation and check the current API documentation.

Will increasing the viewport always fix the cutoff?

No. It can help diagnose a viewport-dependent layout, but first determine whether the page is constrained or the content lives in a nested scroller.

Can I take one full screenshot of a virtualized list?

Only if all required items are present together in the rendered page. A virtualized list may discard off-screen rows, requiring section capture or another data source.