Fix Repeated Sticky Headers in Puppeteer Screenshots
A sticky header can repeat in a full-page screenshot when a capture script scrolls and stitches the page. Identify the capture method, then choose a fix that fits the image you need.
If a header repeats down a Puppeteer screenshot, first check how the image is captured. A normal viewport screenshot shows only the current viewport. Puppeteer’s native fullPage: true captures the full page extent. A scroll-and-stitch helper captures multiple viewport images and combines them; unless it handles sticky elements, the header can appear in each slice.
For a faithful image of what a visitor sees, take a viewport screenshot. For one static image of the whole document, inspect the header’s CSS and scrolling ancestors. If your capture scrolls and stitches, temporarily adjust sticky positioning during capture, then inspect the layout. captureBeyondViewport: false helped one historical report, but Puppeteer does not document it as a general sticky-header fix.
The title mentions an Indian website, but the relevant behavior is determined by the page’s CSS, scrolling context, capture method, and browser versions. The available evidence does not establish a distinct India-specific cause.
1. Identify the screenshot method
Record the Puppeteer and Chromium versions, the exact screenshot call, and whether a wrapper or helper library is involved. Then compare the output you want with the method producing it:
| Desired output | Capture method | What to check |
|---|---|---|
| What the visitor sees now | Viewport screenshot, with fullPage omitted or false |
Only the visible viewport is captured. If the header repeats here, inspect the page or application before investigating full-page capture. |
| One image covering the document | Native fullPage: true |
Puppeteer documents full-page extent, not a CSS rewrite. Check the result against your installed Puppeteer and Chromium versions. |
| A stitched image made from viewport captures | Scroll, capture, and combine | Sticky and fixed elements may occur in successive slices. Check whether the helper offers a capture-time CSS override. |
| A case resembling the 2021 issue report | Controlled comparison with captureBeyondViewport: false |
Treat this as an experiment for that setup, not a universal remedy. |
Puppeteer’s ScreenshotOptions documentation describes fullPage and captureBeyondViewport as capture options. A historical issue report says the latter option fixed one particular sticky-menu case. A separate maintainer discussion describes the trade-offs of full-page capture approaches, including repeated fixed or sticky elements in scroll-and-stitch workflows.
2. Inspect the header and its scrolling context
A sticky element sticks relative to its nearest ancestor with a scrolling mechanism. That ancestor can have overflow: hidden, scroll, auto, or overlay, even when a different element appears to be the one scrolling. Sticky positioning also needs a non-auto inset on the relevant axis, such as top: 0. See MDN’s documentation for the position CSS property.
In browser devtools, inspect the header and its ancestors. Check the computed position, the relevant inset, and each ancestor’s computed overflow. Also check whether the screenshot helper changes viewport size, scroll position, or styles. Those details help distinguish a real page behavior from an artifact of capture.
The following diagnostic can be run in Puppeteer after the page loads. Replace the selector with the header’s selector:
const diagnosis = await page.evaluate(() => {
const header = document.querySelector('header');
if (!header) return { error: 'Header selector did not match' };
const describe = (element) => {
const style = getComputedStyle(element);
return {
tag: element.tagName.toLowerCase(),
id: element.id || null,
className: typeof element.className === 'string' ? element.className : null,
position: style.position,
top: style.top,
overflow: style.overflow,
overflowX: style.overflowX,
overflowY: style.overflowY
};
};
const ancestors = [];
for (let node = header; node; node = node.parentElement) {
ancestors.push(describe(node));
}
return { header: describe(header), ancestors };
});
console.log(JSON.stringify(diagnosis, null, 2));
3. Capture the viewport or full page with Puppeteer
Install Puppeteer with npm install puppeteer. This complete Node.js example writes a viewport screenshot and a native full-page screenshot. Set TARGET_URL to the page you are authorized to capture. It also prints the header diagnosis. The example uses the current Puppeteer API shape; consult the installed version’s documentation if an option behaves differently in your environment.
const puppeteer = require('puppeteer');
(async () => {
const targetUrl = process.env.TARGET_URL || 'https://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto(targetUrl, { waitUntil: 'networkidle2', timeout: 60000 });
const diagnosis = await page.evaluate(() => {
const header = document.querySelector('header');
if (!header) return { error: 'Header selector did not match' };
const ancestors = [];
for (let node = header; node; node = node.parentElement) {
const style = getComputedStyle(node);
ancestors.push({
tag: node.tagName.toLowerCase(),
id: node.id || null,
position: style.position,
top: style.top,
overflow: style.overflow,
overflowY: style.overflowY
});
}
return { ancestors };
});
console.log('Header diagnosis:', JSON.stringify(diagnosis, null, 2));
// What is visible at the current scroll position.
await page.screenshot({ path: 'viewport.png' });
// One native full-page image.
await page.screenshot({ path: 'full-page.png', fullPage: true });
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
fullPage expands capture extent; it does not promise to change the header’s CSS positioning. If repeated headers occur only in a helper that scrolls and stitches, test that workflow separately from native fullPage.
4. Handle sticky positioning in a scroll-and-stitch capture
If successive viewport screenshots are stitched, a sticky header can be visible in each one by design. For a static document image, apply a temporary capture-only style that returns the header to the intended document flow. Choose the selector carefully: a broad rule may also change unrelated navigation or layout.
For example, if the page has a single header that should occupy its normal place in the document, add a style before the stitching helper captures its slices:
await page.addStyleTag({
content: `
header.site-header {
position: static !important;
inset: auto !important;
}
`
});
Replace header.site-header with the actual selector. Use the override only for the capture; do not use it for a screenshot meant to show the live, sticky experience. A visibility-only rule such as visibility: hidden can leave an empty gap because the element may remain positioned outside normal flow. Check whether the resulting image preserves the intended content position.
A scroll-and-stitch package specifically warns that sticky elements can appear multiple times and documents custom styles as a way to reset them: see puppeteer-full-page-screenshot documentation. Confirm how your chosen helper applies styles and when it takes each slice.
5. A/B test captureBeyondViewport only when relevant
Test this option only after recording the browser version, Puppeteer version, screenshot options, and reproduction steps. Compare the same page and viewport with the option omitted and set to false:
await page.screenshot({ path: 'baseline.png', fullPage: true });
await page.screenshot({
path: 'capture-beyond-viewport-false.png',
fullPage: true,
captureBeyondViewport: false
});
Inspect both images for completeness, repeated elements, clipping, and layout changes. Puppeteer’s current documentation describes the default for captureBeyondViewport as false when there is no clip and true otherwise, and defines the option in terms of capturing beyond the viewport. The historical issue report’s success with false is evidence for one configuration, not a general sticky fix. If the setting has no effect in your case, remove it and continue with the capture-method diagnosis.
6. Choose the approach that matches the image
- Visitor view: capture the viewport at the desired scroll position and leave the sticky behavior intact.
- Static whole-document image: try native
fullPage: truefirst; inspect the result in your installed versions. - Stitched full-page image: apply a narrowly scoped capture-time style or use the helper’s documented sticky-element handling. Check for gaps and shifts.
- Unexplained rendering difference: compare
captureBeyondViewportin a controlled reproduction, and record versions and options before changing other variables.
Changing viewport height or width can change layouts that depend on viewport units such as vh, and can affect sticky or fixed positioning. Scroll-and-stitch has a different trade-off: repeated elements and extra capture work. These are reasons to inspect the actual output rather than assume two full-page methods are equivalent; the Puppeteer maintainer discussion is historical context, not a guarantee about every current implementation.
7. Troubleshoot common failures
| Symptom | Likely cause | What to do |
|---|---|---|
| Header repeats only in the stitched output | The helper scrolls and captures successive viewports without handling sticky positioning. | Use native full-page capture if it meets the need, or apply a capture-time style before stitching and inspect the layout. |
fullPage: true still shows unexpected header behavior |
Full-page extent does not mean Puppeteer rewrites CSS positioning; behavior can depend on page and browser versions. | Record versions, inspect computed styles and ancestors, then compare with a viewport capture. |
captureBeyondViewport: false changes nothing |
The historical workaround does not apply to this page or capture path. | Do not treat it as required. Identify whether the method is native capture or stitching and test that method directly. |
| CSS override removes the header but leaves a large gap | The header is hidden visually but still affects or occupies layout, or the chosen positioning change does not match the page structure. | Inspect its original positioning and document flow. Use a targeted override that puts it in the intended flow, then compare content alignment. |
| Another element sticks or moves after the override | The selector matched more than the intended header, or an ancestor is the actual scrolling context. | Narrow the selector and inspect computed styles and overflow on each ancestor. |
| Navigation or content is clipped in a stitched image | The helper’s scroll offsets, viewport size, or stitching boundaries do not match the page’s layout. | Check slice boundaries and scroll positions; compare with a native full-page screenshot at the same viewport width. |
| Navigation times out before the screenshot | The page may keep network activity open, making a network-idle condition unsuitable. | Try a different documented waitUntil condition and wait for a specific selector or application-ready signal before capture. |
| Header is missing even in a viewport screenshot | The capture may occur before rendering, at an unexpected scroll position, or after site behavior changes the header. | Wait for the header selector, check its visibility and position, and capture at the intended scroll position. |
8. Performance, reliability, and cost
A viewport screenshot usually requires less capture work than a page assembled from many viewport images. Stitching also requires repeated scrolling and captures; it can add time and create more opportunities for content to change between slices. Full-page image size and capture time depend on the page and output dimensions, so measure the pages in your own workload rather than relying on a universal benchmark.
For reliable comparisons, keep the URL, viewport dimensions, device scale factor, browser versions, wait condition, and page state fixed. Record the screenshot options and whether a wrapper changes styles or scroll position. If a page updates while slices are being collected, the stitched image may combine different states; wait for the page’s relevant content to settle and use a consistent readiness condition.
Browser automation has compute and maintenance costs: you need a compatible browser runtime, dependencies, and code to handle navigation and capture errors. The dossier provides no benchmark or price comparison for local Puppeteer, so estimate cost from your environment and capture volume.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request captures a URL as an image or PDF. For a WebP shot of the same target page, see the ScreenshotNeo API documentation and use:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Equivalent Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Equivalent Node.js:
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', bytes);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; each cleanup 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 in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does this happen because the site is Indian?
The cited behavior is explained by CSS scrolling context and screenshot method. The available sources do not establish an India-specific cause.
Should I always remove sticky headers before taking a screenshot?
No. Keep them for a viewport image that should match what a visitor sees. Consider a temporary override only when producing a static whole-document image from repeated viewport captures.
Will a CSS override change the live website?
A style added to the Puppeteer page changes that browser page during capture. It does not edit the site’s source files. Remove or avoid the override when the intended image should show the live sticky behavior.
Can I assume a native full-page screenshot and a stitched screenshot are equivalent?
No. They use different capture approaches and can render sticky elements differently. Compare the output for the Puppeteer and Chromium versions you deploy.


