ScreenshotNeo

BlogHow-to

Fix Puppeteer Screenshots That Cut Off Sticky Headers on Long Webpages

Choose the right Puppeteer screenshot mode, then diagnose clipping, sticky positioning, viewport changes, and version-specific behavior.

By the ScreenshotNeo team4 October 20269 min read

Start by matching the screenshot mode to the image you need. For one tall image of the document, use page.screenshot({ fullPage: true }). For a normal browser view, capture the viewport without fullPage. Use clip only when you intentionally want a specific region. These modes behave differently, and none is a universal fix for a sticky header that appears clipped, misplaced, or repeated.

A full-page screenshot is a static image of the page, not a series of screenshots that reproduce the header at each scroll position. If the result is wrong, check the screenshot options, viewport, header CSS, timing, and installed Puppeteer and browser versions before changing the page.

1. Identify the output you need

Goal Approach Trade-off
One tall image of the document page.screenshot({ fullPage: true }) A static full-page capture does not represent the header repeatedly sticking during scroll.
The browser’s visible composition Capture the viewport at the intended scroll position. The output contains only what fits in that viewport.
A specific page region Use clip with an explicit rectangle. Check the rectangle and captureBeyondViewport behavior against the viewport.
A tall element Use ElementHandle.screenshot() when the target is an element. This is not documented as a substitute for full-document capture.
Several realistic scroll states Capture viewport frames at chosen scroll positions and stitch them if needed. Sticky and fixed elements can recur in multiple frames.

Puppeteer documents fullPage, clip, and captureBeyondViewport as separate screenshot controls. fullPage defaults to false. captureBeyondViewport defaults to false when no clip is supplied and true otherwise. See the Puppeteer ScreenshotOptions reference.

2. Reproduce the capture with explicit settings

Record the exact Puppeteer package version, browser version, headless mode, viewport dimensions, device scale factor, and screenshot options. Set the viewport before navigation, then wait for the page to finish the layout changes relevant to your capture.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.setViewport({
      width: 1280,
      height: 900,
      deviceScaleFactor: 1,
    });
    await page.goto('https://example.com/long-page', {
      waitUntil: 'networkidle2',
    });

    // Viewport screenshot: the browser's visible composition.
    await page.screenshot({ path: 'viewport.png' });

    // Full document screenshot: one tall image.
    await page.screenshot({ path: 'full-page.png', fullPage: true });

    // Intentional region capture; coordinates are in CSS pixels.
    await page.screenshot({
      path: 'region.png',
      clip: { x: 0, y: 0, width: 1280, height: 600 },
      captureBeyondViewport: true,
    });
  } finally {
    await browser.close();
  }
})();

Replace the example URL with the page you are diagnosing. For a reliable comparison, use a long page with one sticky header, hold all settings constant, and compare the default viewport capture, the full-page capture, and the same clip used by your application. Check output dimensions and where the header appears.

For a script using ES modules, use import puppeteer from 'puppeteer'; and keep the same capture logic. The example uses CommonJS so it can run directly in a typical Node.js project with Puppeteer installed.

3. Check clipping and screenshot options

Search the capture code and wrappers for clip, including values added indirectly by helper functions. A clip rectangle that is shorter or offset from the expected region can make a header look cut off. Check that its x, y, width, and height use the intended CSS-pixel coordinates and are not derived from stale dimensions.

Keep fullPage and clip conceptually separate: full-page capture requests the document, while clip selects a region. If the production capture has a clip, reproduce it exactly. Also set captureBeyondViewport explicitly while diagnosing so its documented default does not depend on whether a clip is present. Do not assume combining options will produce the same image as a viewport capture.

The Puppeteer screenshots guide also documents element screenshots. An element screenshot attempts to scroll a hidden element into view; that behavior does not promise that an arbitrarily tall element will be captured as a substitute for fullPage: true.

4. Separate sticky CSS from capture behavior

A sticky header is positioned relative to scrolling and may remain attached to the viewport. A fixed header is likewise viewport-attached. Decide whether that behavior belongs in the target image:

  • For one clean, static long image: test a temporary capture-only style that changes the header’s position: sticky or position: fixed behavior to normal document flow. Compare the result with the unmodified capture. Apply this only if the static layout is the image you want.
  • For a faithful viewport image: preserve the site CSS and capture at the intended viewport and scroll position.
  • For stitched scroll frames: expect a sticky or fixed header to appear in more than one frame unless your stitching process handles it. Hiding duplicates changes what the browser displayed.

A capture-only style is a troubleshooting strategy, not a universal Puppeteer-prescribed fix. If you inject a style, keep it scoped to the capture, confirm that the page is restored afterwards, and inspect the top, middle, and bottom for layout shifts or missing content.

5. Treat viewport resizing as a separate variable

Set viewport dimensions deliberately and before navigation. Changing the viewport can switch responsive breakpoints, change sections sized with vh, and alter where sticky or fixed elements sit. A larger viewport may help diagnose a clipping symptom, but it can also change the design being captured. Compare images at the intended production viewport before adopting a resize workaround.

Wait for application layout and content to settle before taking the screenshot. If the page resizes around capture, investigate responsive scripts, animations, lazy-loaded content, and other app code that can change layout. Keep the viewport, device scale factor, and wait condition identical between comparison captures.

6. Check the Puppeteer and Chromium version context

Screenshot behavior has changed over Puppeteer’s history. In a 2019 discussion about Puppeteer v2.0.0, a maintainer described a Chromium change that made screenshot clipping consistent with viewport clipping; the discussion also notes that earlier behavior could put fixed-position elements in unexpected places. The discussion mentions viewport resizing for scripts that relied on the old behavior, while warning that different use cases may need different solutions. Treat it as historical context, not as a current-version prescription: Puppeteer issue #5080.

A separate 2021 report described a resize symptom with Puppeteer 8.0.0 and a custom default viewport plus device scale factor 2. It is one historical report, not evidence of a current general defect: Puppeteer issue #7043.

When comparing a working and failing environment, write down both the Puppeteer package version and the browser version it launches. Do not copy an old launch flag or workaround without checking that it applies to the installed release.

7. Diagnose systematically

  1. Write down the desired artifact: full document, viewport, clipped region, element, or stitched scroll states.
  2. Record the environment: Puppeteer and browser versions, headless mode, viewport width and height, device scale factor, and every screenshot option.
  3. Reduce the page: use a long static document with one sticky header and a known viewport.
  4. Capture a baseline: save a viewport screenshot with no clip and default options.
  5. Compare modes: save a full-page screenshot, then reproduce the production clip if there is one. Change one variable per capture.
  6. Inspect CSS and timing: check header positioning, responsive rules, layout changes, animations, and lazy loading.
  7. Test a capture-only header style only if appropriate: use it for a static long image when removing viewport attachment matches the desired result.
  8. Verify the output: inspect the top, middle, and bottom for cut edges, repeated headers, layout shifts, and missing content. Confirm any temporary style is reversed.

8. Troubleshooting common symptoms

Symptom Likely cause What to check or change
Only the visible portion is saved fullPage is omitted or false. For one tall document image, set fullPage: true. Confirm the requirement is not a viewport image.
The top or bottom of the header is cropped A clip rectangle, viewport boundary, or unexpected dimensions constrain the result. Remove clip for a baseline; then verify clip coordinates, viewport dimensions, and captureBeyondViewport.
The header appears at an unexpected vertical position Sticky/fixed CSS interacts with the selected capture mode, or the layout changed before capture. Compare viewport and full-page captures with identical settings; inspect header CSS and timing.
The header repeats down a long image Frames were stitched while the header remained sticky or fixed. For a static image, test a capture-only style that puts the header in document flow. For faithful scroll states, repetition may reflect the browser view.
The page looks different after increasing viewport size Responsive breakpoints or viewport-relative CSS changed the layout. Restore the target viewport and diagnose at that size. Do not treat a larger viewport as visually equivalent.
The result differs after a Puppeteer upgrade Puppeteer or its Chromium version may have changed capture behavior. Record both versions and compare the same reduced page and options; consult version-relevant issue history before applying a workaround.
The header moves or content is missing intermittently Layout, animation, lazy loading, or application behavior is still changing. Use a reproducible wait condition, hold viewport and scale constant, and compare captures after the page settles.
A tall element screenshot is incomplete Element capture is being treated as full-document capture. Use fullPage: true for the page document, or test the element capture against the element’s actual dimensions and intended output.

9. Performance, reliability, and cost considerations

A single full-page capture is the simplest way to request a tall page image, but the resulting image can be much taller than a viewport screenshot. For long pages, consider whether the consumer needs the entire document at once or only selected viewport states. Stitched frames introduce extra capture and image-composition work, and sticky elements can recur. The cited Puppeteer documentation and issue history do not provide a benchmark for these approaches, so measure them on the pages and runtime that matter to your application.

For repeatable captures, pin the Puppeteer dependency in your project, record the browser version, set viewport and scale explicitly, and keep the capture options in logs. Use a reduced reproducible page when upgrading. This makes a version change easier to distinguish from a CSS or timing change.

Cost depends on where and how you run the browser; the supplied Puppeteer documentation and issue material do not establish a general cost or speed figure. Account for browser runtime, image storage or transfer, and any extra frame captures in your own deployment.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Send one GET request with a URL to receive an image or PDF; see the API documentation for options. For this example, the returned file is a WebP screenshot of the target page:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/long-page -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/long-page"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/long-page',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
  writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month with no card.

FAQ

Does fullPage: true mean the sticky header is repeated while scrolling?

No. It requests a full-page screenshot; it does not ask Puppeteer to reproduce each browser scroll state.

Should I always remove sticky positioning before capture?

No. Only test that for a static long image when a header in normal document flow matches the desired output. Preserve sticky behavior for a faithful viewport screenshot.

Can I use an element screenshot instead?

Use it when the target is an element. The documented behavior includes scrolling a hidden element into view, but it is not a guarantee of full-document capture.

Is a historical Puppeteer issue proof that my current version has the same bug?

No. Confirm the installed package and browser versions, then reproduce with explicit options and a reduced page.