ScreenshotNeo

BlogHow-to

Chrome Headless Screenshot with a Fixed Header: Prevent Duplicate Headers

Stop fixed and sticky headers from repeating in full-page Chrome screenshots. Identify the capture method, apply a page-specific CSS override, and check the result.

By the ScreenshotNeo team4 October 20267 min read

A fixed or sticky header can appear multiple times when a full-page screenshot is assembled from several viewport captures: the header stays visible as the page scrolls, so each slice can include it. First identify whether your tool takes one full-page capture or scrolls and stitches multiple images. For a stitched capture, temporarily reset the specific header’s positioning before capture, then restore its original style. There is no documented Chrome switch that universally prevents duplicate headers.

1. Identify how the screenshot is captured

The remedy depends on the capture path. Chrome’s command-line --screenshot saves a screenshot of the target page, and --window-size sets the capture dimensions. Puppeteer documents page.screenshot({ fullPage: true }) as a full-page capture, and also supports capturing an individual element. A scroll-and-stitch library instead takes several viewport images and joins them. That method can repeat sticky elements; the puppeteer-full-page-screenshot package calls out this behavior and recommends custom styles that reset sticky positioning.

Do not assume all full-page implementations behave the same way. Record the tool, browser version, viewport width and height, and whether the duplicate is visible in the original output file or introduced by later image processing. The Puppeteer options describe capture scope; they do not promise a universal fixed-header correction.

2. Capture a baseline with Chrome Headless

Use a fixed viewport so you can compare captures consistently. Replace the example URL with the page you are authorized to capture.

chrome --headless --screenshot --window-size=1440,1000 https://example.com/

Chrome saves screenshot.png in the current working directory. This command is useful for a baseline, but it does not expose a special option for disabling fixed positioning. The Chrome documentation recommends --window-size alongside --screenshot when dimensions matter. See the Chrome Headless command-line reference.

For Chrome version context, Chromium’s documentation says old Headless functionality stopped being part of the Chrome binary as of M132; users relying on that old implementation should migrate to chrome-headless-shell. This is a tooling compatibility note, not a fix for duplicated headers. See the Chromium Headless README.

3. Use Puppeteer and reset the header for a stitched capture

If the page belongs to you, the cleanest fix is to temporarily change the header’s positioning for the screenshot. Pick a selector that uniquely targets the relevant header. Resetting it to normal document flow usually preserves a single header at the top while removing its viewport-pinned behavior. Hiding it is another option, but may leave a blank gap or remove content the image needs.

The following runnable Node.js example uses Puppeteer’s own full-page mode and applies a temporary style. It assumes the target header is header.site-header; change the selector for your site. The style is restored in a finally block even if capture fails.

import puppeteer from 'puppeteer';

const url = process.argv[2] ?? 'https://example.com/';
const headerSelector = 'header.site-header';
const browser = await puppeteer.launch({ headless: true });

try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
  await page.waitForSelector(headerSelector, { timeout: 10000 });

  await page.addStyleTag({
    content: `${headerSelector} {
      position: static !important;
      inset: auto !important;
      transform: none !important;
    }`,
  });

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

Run it with node capture.mjs https://example.com/ after installing Puppeteer in your project. This example changes the page only inside the browser instance used for the capture. If the header’s layout depends on sticky positioning, test whether position: relative or another page-specific rule preserves the desired placement better than static. Inspect the result for overlapping content, missing navigation, or a large gap where the header used to be.

If you use a library that explicitly scrolls and stitches the page, apply the CSS override immediately before that library begins its capture. The package’s warning is specific to its stitched method; do not generalize it to every Chrome or Puppeteer full-page screenshot.

4. Choose the right Puppeteer capture scope

Goal Approach What to check
Capture what is visible at one viewport page.screenshot({ path: 'viewport.png' }) The header appears once as it appears in the viewport.
Capture the full document page.screenshot({ path: 'full.png', fullPage: true }) Check output dimensions and header behavior for your page and Puppeteer version.
Capture a particular component Find the element and call element.screenshot({ path: 'element.png' }) The element may be scrolled into view before capture.
Work around tall-page or viewport-relative layout issues Consider a scroll-and-stitch library with a targeted CSS reset. Sticky elements can recur in each stitched segment; verify the merged image.

Puppeteer documents fullPage and captureBeyondViewport as screenshot options, and documents ElementHandle.screenshot() for a specific element. Changing these options changes capture behavior, but the documentation does not promise that any one option fixes fixed or sticky headers on every page. See Puppeteer ScreenshotOptions and the Puppeteer screenshots guide.

5. Troubleshoot by symptom

Symptom Likely cause Action
Header repeats at regular intervals down the image Capture tool scrolls and stitches viewport screenshots while the header remains sticky or fixed. Confirm the tool’s implementation; apply a temporary, page-specific positioning reset before its capture.
Header disappears after applying an override The selector matched the wrong element, or the override hid or displaced the header. Use browser inspection to target the actual header and reset positioning rather than hiding it. Check the output before keeping the rule.
A blank band remains at the top Removing sticky behavior changed positioning, while spacing or a reserved header container remains. Inspect the header and its wrapper. Adjust the page-specific capture CSS to preserve the intended flow and spacing.
Override has no visible effect Selector is incorrect, style is applied before navigation, or a later rule or script changes the header. Wait for the page and header, verify the selector matches, and apply the capture style immediately before taking the screenshot.
Screenshot looks different between runs Viewport dimensions, browser version, page loading state, or dynamic content differs. Fix the viewport and wait condition; record Chrome/Puppeteer versions and compare the same URL and page state.
Capture times out or page is incomplete The page has slow or continuously active network requests, or the configured timeout is too short. Choose a wait condition appropriate for the site, set a bounded timeout, and wait for a specific content selector when possible.
Old Headless flag no longer behaves as expected Old Headless was removed from the Chrome binary in M132. Use current Headless mode or migrate an old Headless-shell workflow to chrome-headless-shell, as applicable.

6. Reliability, performance, and cost

A single full-page capture avoids writing your own stitching logic, while a stitched capture can be useful when the page is unusually tall or relies on viewport-relative sizing. The trade-off is that every slice can encounter scrolling behavior, dynamic content changes, lazy-loaded images, and repeated fixed elements. For a reproducible result, pin the viewport, use a deliberate wait condition, apply CSS only to the capture session, and inspect the final image.

Very tall pages require larger image buffers and can take longer to capture or merge. Limit the capture to the required page or element when a full document is unnecessary. For an automated pipeline, keep timeouts bounded, close the browser in a cleanup path, and save a diagnostic copy when an output fails visual review. Chrome Headless and Puppeteer have no per-screenshot price described in the cited documentation; operational cost depends on the infrastructure and any services you choose to run.

7. Common capture choices

For a controlled site and a repeatable screenshot job, Puppeteer gives you direct access to a page and lets you add a temporary CSS rule. Chrome’s CLI is a small baseline option when you need a simple page capture with a known viewport. A stitched package can help with particular tall-page layouts, but its own documentation warns that sticky elements may repeat. Choose based on whether you need viewport, full-page, or element capture and how much page-specific control you have.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a screenshot or PDF; use the API documentation for its options. The API’s cleanup can accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing status. An MCP server lets AI agents use screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

The Node.js snippet uses Bun’s file-writing helper; in Node.js, save the response body with await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))). ScreenshotNeo does not require you to launch and configure a browser for this request. Sign up for 1,000 free screenshots a month with no card.

FAQ

Is a repeated header always caused by CSS?

No. First establish whether the capture tool itself scrolls and stitches or whether an image-processing step duplicates a region. The page’s positioning is one likely cause in stitched captures.

Should I remove the header from the screenshot?

Only if the header is not part of the image you need. Resetting its positioning often keeps one header in the document; hiding it can remove useful navigation or leave unwanted spacing.

Does captureBeyondViewport prevent duplicates?

Puppeteer documents it as control over capturing beyond the viewport. Its API reference does not describe it as a fixed-header repair.

Can I use this workaround on a site I do not control?

You can inject a style in your own browser automation if the page permits it, but selectors and layout vary by site. Check the resulting image and respect the site’s access requirements.