ScreenshotNeo

BlogHow-to

Fix Puppeteer Screenshots with a Misaligned Web Page After Scrolling

Find out why a Puppeteer screenshot shifts after scrolling. Check element auto-scrolling, capture options, viewport timing, and page layout stability.

By the ScreenshotNeo team4 October 20266 min read

If a Puppeteer screenshot no longer lines up with the page after scrolling, first identify which screenshot method you call. ElementHandle.screenshot() scrolls its element into view if needed before capturing it; Page.screenshot() captures the page according to its screenshot options. Then check fullPage, clip, captureBeyondViewport, the viewport size, and whether the page changes layout after scrolling.

There is no universal one-line fix: the cause depends on your capture code, Puppeteer and browser versions, screenshot options, and the page’s behavior. The steps below help isolate it.

1. Identify what is being captured

Compare the call site with the artifact you want:

Call or option What to check
page.screenshot() Check whether you want the current viewport or the full document, and review any clip.
elementHandle.screenshot() It may scroll the element into view first. Its scrollIntoView option defaults to true.
fullPage: true Captures the full page rather than only the viewport. The default is false.
clip Defines a capture rectangle. Verify its coordinates and dimensions match the intended region.
captureBeyondViewport Defaults to false without a clip and true with one. Confirm this behavior for the Puppeteer version installed in your project.

These behaviors are documented by Puppeteer; they identify what to inspect, but do not prove which factor caused a particular mismatch. See the official ElementHandle screenshot API and ScreenshotOptions API.

2. Make viewport setup consistent

Set the viewport before navigation when possible. Puppeteer notes that some sites do not expect their viewport to change after loading, and certain mobile or touch settings can trigger a page reload. A viewport change can also alter responsive layout, so compare the viewport used during navigation with the one used at capture time.

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

console.log('viewport:', page.viewport());
console.log('scroll:', await page.evaluate(() => ({ x: scrollX, y: scrollY })));
await page.screenshot({ path: 'viewport.png' });
await browser.close();

example.com is a placeholder; replace it with the page you are debugging. For a controlled comparison, keep viewport dimensions and device scale factor fixed and record them alongside the capture. Puppeteer’s setViewport API documents viewport resizing and its behavior.

3. Compare the page before and after scrolling

Record the scroll position and target bounds immediately before the screenshot. Capture once before scrolling and once afterward. If the element’s bounding box or the page’s layout changes, investigate page behavior as well as screenshot options.

async function snapshotState(page, selector) {
  return page.evaluate((sel) => {
    const element = document.querySelector(sel);
    const rect = element?.getBoundingClientRect();
    return {
      scrollX,
      scrollY,
      viewport: { width: innerWidth, height: innerHeight },
      target: rect ? { x: rect.x, y: rect.y, width: rect.width, height: rect.height } : null,
      document: { width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight },
    };
  }, selector);
}

const selector = '.target';
console.log('before:', await snapshotState(page, selector));
await page.evaluate(() => window.scrollTo(0, 600));
console.log('after scroll:', await snapshotState(page, selector));
await page.screenshot({ path: 'after-scroll.png' });

Replace .target and the scroll amount with values from your page. This logging is a debugging technique, not a guarantee that Puppeteer or the page will remain stationary after the measurements.

4. Wait for the page’s layout to settle

Scrolling can coincide with page-level changes such as sticky headers, lazy-loaded content, animations, responsive breakpoints, or scroll-triggered DOM updates. These are possibilities to check, not causes established by Puppeteer documentation. Wait for a condition that is meaningful for your page—for example, a target element appearing or an application-specific loading indicator disappearing—then capture.

await page.evaluate(() => window.scrollTo(0, 600));
await page.waitForSelector('.target', { visible: true });
// Add a page-specific readiness check here if the page updates after scrolling.
await page.screenshot({ path: 'settled.png' });

Puppeteer locators can scroll an element and wait for stable bounding boxes over two consecutive animation frames before acting. That is a useful signal when investigating movement, but Puppeteer does not describe it as a universal guarantee that every page has finished changing before a screenshot. See the official page interactions guide.

5. Capture an element deliberately

If the desired result is one element, use its screenshot method and decide whether its automatic scroll is appropriate. The element screenshot options document scrollIntoView, which defaults to true.

const target = await page.waitForSelector('.target', { visible: true });
if (!target) throw new Error('Target element was not found');
await target.screenshot({ path: 'element.png', scrollIntoView: false });

Use scrollIntoView: false only when the element is already in the intended position and you specifically want to avoid the helper’s scroll. Otherwise, leave the default behavior and account for the scroll before capture. Check the installed version’s API if an option is unavailable.

6. Choose viewport, full-page, or clipped capture

Match screenshot options to the artifact you need. A viewport image represents the visible area; a full-page image covers the page; a clip captures a chosen rectangle.

// Current viewport
await page.screenshot({ path: 'viewport.png' });

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

// A fixed region
await page.screenshot({
  path: 'region.png',
  clip: { x: 0, y: 200, width: 800, height: 500 },
});

Do not combine these modes casually. Review fullPage, clip, and captureBeyondViewport together, and verify the options against the Puppeteer version you run. The documented defaults are not a diagnosis of your application’s layout.

7. Troubleshooting checklist

Symptom Likely area to inspect Next step
Element shot is offset after calling its screenshot method The method may have scrolled the element into view. Log scroll position and bounds before capture; decide whether scrollIntoView should be enabled.
Only the full-page image differs fullPage changes the capture extent. Compare against a viewport capture and verify the intended output.
A clipped image is shifted or cropped Clip coordinates or capture-beyond-viewport behavior. Log the clip values, compare with the viewport, and check captureBeyondViewport.
Layout changes after navigation or resize Viewport was changed late, or mobile/touch settings caused reload behavior. Set viewport before navigation and keep capture dimensions consistent.
The target moves after the scroll Page-specific layout or scroll-triggered updates. Compare bounds before and after scrolling and wait for a page-specific ready condition.
Results differ between environments Browser/Puppeteer version, viewport, device scale, or page state may differ. Record versions and options, then reproduce with the same configuration.
Target cannot be found Selector mismatch, delayed rendering, or an iframe/shadow-root boundary. Verify the selector in the correct frame and wait for the actual page condition.

8. Performance, reliability, and cost

For diagnosis, first capture the smallest artifact that answers the question: a viewport or target element is easier to compare than a full page. Full-page capture can involve more page content, so consider its image dimensions and memory use in your own workload. Keep viewport, browser version, scale factor, and capture options fixed when comparing runs.

Reliability depends on the page reaching a repeatable state. Prefer explicit readiness conditions that reflect the target content over arbitrary delays when the application provides a better signal. Puppeteer documentation does not supply a universal settling time or performance benchmark for this scenario. Your costs depend on where and how you run the browser; measure resource use in your environment.

Or skip the browser setup

For a managed screenshot call, ScreenshotNeo accepts a URL and returns an image or PDF. See the API documentation for its request options.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp
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)
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}`);
await Bun.write('shot.webp', res);

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

FAQ

Does scrolling always cause Puppeteer screenshots to shift?

No. The screenshot API, options, viewport, and page behavior all matter. Compare the state immediately before capture to locate when the geometry changes.

Should I always use fullPage: true?

No. Use it only when you need the full document; use a default screenshot for the viewport or a clip for a specific region.

Can stable bounding boxes prove the screenshot will not move?

No. Puppeteer locators use a two-animation-frame stability check for actions, but that does not guarantee all application layout changes have stopped.

Which Puppeteer version should I check?

Check the version installed in the affected project and confirm the relevant options and defaults in that version’s documentation. The research references documentation version 25.12.0.