ScreenshotNeo

BlogHow-to

Puppeteer Full-Page Screenshot Is Missing the Bottom of a CSS Grid

A missing grid row does not prove CSS Grid is the cause. Measure the document and grid bounds, then check capture options, clipping, layout, and timing.

By the ScreenshotNeo team4 October 20267 min read

A missing bottom row in a Puppeteer full-page screenshot does not by itself mean CSS Grid is broken. First confirm that the capture uses fullPage: true, then compare the document’s scroll height with the grid and last-row bounds. If the document includes the row but the image does not, investigate capture timing, clipping, viewport-sensitive styles, and the exact Puppeteer and Chromium versions. The supplied evidence does not identify one universal CSS Grid fix for this symptom.

1. Confirm that Puppeteer is capturing the full page

Puppeteer’s current ScreenshotOptions API documents fullPage as taking a screenshot of the full page when true; its default is false. Set it explicitly:

await page.screenshot({ path: 'page.png', fullPage: true });

The separate captureBeyondViewport option is also documented. Do not assume changing it will fix a document-height or CSS layout issue. Check the exact options in your installed Puppeteer version and record the Puppeteer and Chromium versions when debugging.

2. Measure document, grid, and final-row bounds

Before changing CSS, determine whether the page layout actually extends to the bottom row. Give the grid and final row stable selectors in your application, then collect their bounds and the document heights:

const measurements = await page.evaluate(() => {
  const grid = document.querySelector('.grid');
  const lastRow = grid?.querySelector('.grid-row:last-child');
  const rect = (element) => {
    if (!element) return null;
    const r = element.getBoundingClientRect();
    return {
      top: r.top,
      bottom: r.bottom,
      height: r.height,
      documentTop: r.top + window.scrollY,
      documentBottom: r.bottom + window.scrollY,
    };
  };
  return {
    documentElementScrollHeight: document.documentElement.scrollHeight,
    bodyScrollHeight: document.body.scrollHeight,
    viewport: { width: innerWidth, height: innerHeight, devicePixelRatio },
    grid: rect(grid),
    lastRow: rect(lastRow),
  };
});
console.log(measurements);

await page.screenshot({ path: 'page.png', fullPage: true });

Replace .grid and .grid-row:last-child with selectors matching the page. The document-relative bounds add window.scrollY to viewport-relative rectangle coordinates. Compare the final row’s document bottom with both scroll heights. If the row extends beyond the measured document extent, focus on layout and overflow. If the document extent includes it but the output omits it, investigate screenshot behavior and reproduce with the exact browser/library pair.

Also inspect the output image dimensions. CSS pixel dimensions and image pixel dimensions may differ with device scale factor; account for devicePixelRatio before concluding that the image was clipped.

3. Inspect the grid’s layout and clipping chain

Check the grid and each ancestor for properties that can constrain or clip content. These are investigation leads, not confirmed causes for this report:

  • Fixed or maximum heights on the grid or a containing block.
  • overflow: hidden, overflow: clip, or another clipping ancestor.
  • A nested scrolling region whose content height is not part of the document’s scroll height.
  • Absolutely or fixed-positioned rows that do not contribute to normal document flow.
  • Viewport-relative dimensions such as vh that change how the page lays out at the capture viewport.
  • Grid content that is rendered outside the grid’s own bounds.

Temporarily outline the grid and final row in a local reproduction, inspect computed styles up the ancestor chain, and compare bounds before and after the screenshot. Change one property at a time so the cause remains identifiable.

4. Wait for the grid to be ready before capture

A page load event or network quiet period does not guarantee that client-side data, images, or layout work is complete. Wait for an application-specific ready condition and for the final row to exist. Puppeteer’s guide demonstrates navigation using waitUntil: 'networkidle2', but use that as a navigation condition rather than proof that application rendering is finished.

await page.goto('https://example.com/grid', { waitUntil: 'networkidle2' });
await page.waitForSelector('.grid .grid-row:last-child');
// If the app exposes a reliable readiness signal, wait for that too.
await page.waitForFunction(() => window.appReady === true);
await page.screenshot({ path: 'page.png', fullPage: true });

Replace the URL and readiness signal with those used by your application. Avoid arbitrary delays unless you are testing a timing hypothesis; a delay can hide a race without making the capture reliable.

5. Distinguish full-page capture from element capture

For a whole document, use page.screenshot({ fullPage: true }). For a specific target, Puppeteer also supports ElementHandle.screenshot():

const grid = await page.$('.grid');
if (!grid) throw new Error('Grid was not found');
await grid.screenshot({ path: 'grid.png' });

Puppeteer documents that element screenshots attempt to scroll a hidden element into view. An older issue report describes clipping when a target extends beyond the viewport, so do not assume element capture handles an arbitrarily tall grid. Verify the resulting image dimensions and behavior on the exact version you deploy.

Enlarging the viewport can be a useful experiment, but it can also trigger media queries and resize handlers and therefore change the page being captured. Compare the page’s layout and measurements at both viewport sizes before treating this as a fix.

6. A runnable diagnostic script

This example navigates to a page, records its browser version and layout measurements, checks that the grid exists, and saves an explicit full-page screenshot. Install Puppeteer in your project and replace the example URL and selectors.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1365, height: 768, deviceScaleFactor: 1 });
    await page.goto('https://example.com/grid', { waitUntil: 'networkidle2' });
    await page.waitForSelector('.grid');
    await page.waitForSelector('.grid .grid-row:last-child');

    const info = await page.evaluate(() => {
      const grid = document.querySelector('.grid');
      const lastRow = grid?.querySelector('.grid-row:last-child');
      const rect = (element) => {
        if (!element) return null;
        const r = element.getBoundingClientRect();
        return {
          top: r.top,
          bottom: r.bottom,
          height: r.height,
          documentTop: r.top + scrollY,
          documentBottom: r.bottom + scrollY,
        };
      };
      return {
        documentElementScrollHeight: document.documentElement.scrollHeight,
        bodyScrollHeight: document.body.scrollHeight,
        viewport: { width: innerWidth, height: innerHeight, devicePixelRatio },
        grid: rect(grid),
        lastRow: rect(lastRow),
      };
    });

    console.log(JSON.stringify({
      puppeteerVersion: require('puppeteer/package.json').version,
      browserVersion: await browser.version(),
      info,
    }, null, 2));

    await page.screenshot({ path: 'page.png', fullPage: true });
  } finally {
    await browser.close();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Keep the viewport, device scale factor, page state, and screenshot options fixed while comparing runs. A useful minimal reproduction includes the grid CSS and content, the viewport, the measured values, the image dimensions, and both version numbers.

7. Troubleshooting

Symptom Likely area to investigate What to do
Screenshot stops at the viewport fullPage is omitted or false Set fullPage: true explicitly and confirm the actual call path uses those options.
Last row is below both document scroll heights Layout extent or clipping Inspect fixed/max heights, overflow, nested scrolling, positioning, and viewport-relative sizing on the grid and ancestors.
Measured document includes the row, but screenshot does not Capture behavior, timing, or browser version Confirm the page is stable, record Puppeteer and Chromium versions, and reduce to a minimal reproduction.
Element screenshot cuts off a tall grid Element capture bounds Try full-page capture if the goal is the document; verify element screenshot output for your exact case and version.
Increasing viewport changes which rows appear Responsive CSS or resize-dependent application logic Inspect media queries and resize handlers; treat viewport changes as experiments, not neutral fixes.
Measurements vary between runs Dynamic content or asynchronous layout Wait for a specific application readiness condition and final-row selector, then compare repeated captures.
Output pixel dimensions seem inconsistent with CSS bounds Device scale factor Record devicePixelRatio and compare CSS pixels with image pixels at the configured scale.

8. Performance, reliability, and cost

Full-page screenshots can require more rendering and image memory as document height grows. Keep the captured page to the required content where possible, and use element capture only when the target and its height are appropriate. For repeatable automation, pin the Puppeteer dependency, record the Chromium version, use a fixed viewport and device scale factor, wait on application state, and save measurements alongside failed artifacts.

Historical issue reports document viewport-sensitive and overflow-related screenshot edge cases, including behavior discussed around older Puppeteer/Chromium versions. They do not establish that those behaviors persist identically in current releases or that CSS Grid is at fault here. Retest old workarounds against the versions you actually ship. The browser automation approach has no per-shot service charge in this workflow, but it does require maintaining the browser runtime and the infrastructure that runs it.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Make one GET request with the target URL; the API returns an image or PDF. Its clean-shot flow accepts the consent banner like a visitor and removes known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents screenshot, page-info, and PDF tools.

See the ScreenshotNeo API documentation for request options and configuration. This example saves the returned image:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com/grid \
  -o shot.webp

One thousand screenshots a month are free with no card; paid plans start at $5 for 3,000. Sign up for the free plan.

10. FAQ

Is this a known CSS Grid bug in Puppeteer?

The available documentation and issue evidence do not identify this exact symptom as a universal CSS Grid bug. Measure the document and row bounds first.

Should I always set captureBeyondViewport?

No. It is a separate documented option; the evidence does not show that toggling it repairs a layout or document-height problem.

What information should I include when asking for help?

Share a minimal page reproduction, relevant grid and ancestor CSS, viewport and scale factor, screenshot options, measured heights and bounds, image dimensions, and Puppeteer/Chromium versions.