GuidesPuppeteer

How to Take a Full-Page Screenshot with Puppeteer

Jun 16, 2026 · 4 min read · ScreenshotInk

A full-page screenshot captures the entire scrollable document — top to bottom, everything below the fold — not just the slice of pixels visible in the viewport. Puppeteer has a built-in option for exactly this, and for simple pages it's a one-liner. The trouble starts when pages lazy-load images, stick headers to the top, or run 10,000 pixels tall. Here's the working script and the things that quietly break.

The quick version

Puppeteer's page.screenshot({ fullPage: true }) resizes the capture to the full scroll height of the document. The minimal script:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: 'new',
  args: ['--no-sandbox'],
});

const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });

await page.goto('https://stripe.com', { waitUntil: 'networkidle2' });

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

await browser.close();

waitUntil: 'networkidle2' waits until there are no more than two in-flight network requests for 500ms, which is a decent proxy for "the page has mostly settled." For most static marketing pages, this produces a correct full-height PNG and you're done.

The parts that break

The one-liner works until it doesn't. Here's what trips people up, roughly in order of how often it bites:

Lazy-loaded images. Plenty of sites only load images as they scroll into view (loading="lazy", IntersectionObserver, infinite-scroll feeds). Because Puppeteer captures full-page from a single viewport position, those images are still blank placeholders when the screenshot fires. The fix is to scroll the whole page first so every image gets requested, then scroll back to the top:

async function autoScroll(page) {
  await page.evaluate(async () => {
    await new Promise((resolve) => {
      let total = 0;
      const step = 400;
      const timer = setInterval(() => {
        window.scrollBy(0, step);
        total += step;
        if (total >= document.body.scrollHeight) {
          clearInterval(timer);
          window.scrollTo(0, 0);
          resolve();
        }
      }, 100);
    });
  });
}

await autoScroll(page);
await page.evaluate(() => new Promise((r) => setTimeout(r, 500))); // let images settle
await page.screenshot({ path: 'out.png', fullPage: true });

Sticky headers that repeat. A header with position: sticky or fixed can appear pinned at multiple points down a tall capture, or smear over content. If you see this, it's usually cleanest to override the offending elements before capturing — position: absolute on the header, or hide it outright with injected CSS via page.addStyleTag.

Very tall pages. Chromium can't paint an arbitrarily large surface. Past a certain height (the practical ceiling is in the tens of thousands of pixels, and it varies), fullPage captures either get cropped or fail silently. Infinite-scroll pages have no natural bottom at all, so you need to cap how far you scroll.

Web fonts not loaded. If the screenshot fires before custom fonts finish loading, text renders in a fallback font and the layout shifts. Wait for fonts explicitly: await page.evaluate(() => document.fonts.ready).

Cookie banners and consent overlays. A GDPR banner glued to the bottom of the viewport will sit on top of your screenshot, and the dimming backdrop washes out the whole page. You have to detect and dismiss these per-site — there's no universal selector — which gets tedious fast across many domains.

When to use an API instead

None of the above is hard in isolation. The cost shows up when you run this in production. Headless Chromium is memory-hungry, crashes under load, and needs careful concurrency control or it'll exhaust the box. Add browser version churn, the per-site cookie-banner and lazy-load handling above, and the fact that scraping at scale gets you rate-limited or IP-blocked — and you're now maintaining a small browser-farm operation, not taking screenshots.

A screenshot API absorbs that work. The equivalent ScreenshotInk call:

curl "https://screenshotink.com/v1/capture" \
  -H "Authorization: Bearer sk_live_…" \
  -d url="https://stripe.com" -d full_size=true -d format=png

It returns JSON pointing at the stored image:

{ "image_url": "https://…full.png", "height": 8260 }

Under the hood it does the things you'd otherwise hand-roll: scrolls to trigger lazy-loaded content, dismisses common cookie banners, and waits for the page to settle before firing. It handles full pages up to 20,000px (full_size=true), outputs PNG, JPEG, or PDF (format=png|jpeg|pdf), and lets you cap height with max_height when you want to bound how far it scrolls. Captures run on EU-hosted infrastructure, and the free tier is 100 captures/month with no card.

So: if you need one screenshot of one page, the Puppeteer script above is fine — copy it and go. If you need thousands of pages reliably, across sites you don't control, without babysitting a Chromium fleet, the API is the simpler answer.

Try it with no signup: full-page screenshot tool. For a quick single capture of any URL, there's also the website screenshot tool.

Capture it with the API

Everything here runs on the ScreenshotInk API — 100 free captures a month, no card.

More guides

The Best Screenshot APIs in 2026, Compared

An honest comparison of the top screenshot APIs in 2026 — ScreenshotInk, ScreenshotOne, ApiFlash, Urlbox and more — on pricing, formats, tooling and AI-agent support.