How to capture full-page screenshots programmatically

How to capture full-page screenshots programmatically

The complete guide to capturing entire scrollable pages as a single image.

March 28, 2026 · 8 min read

A standard screenshot captures only the visible viewport — typically 1280×720 or 1440×900 pixels. But many pages scroll for thousands of pixels. Full-page capture stitches the entire scrollable content into a single image, giving you the complete visual representation of a page.

How full-page capture works

Modern headless browsers handle this natively. When you set fullPage: true, the browser measures the total scroll height of the page, resizes its internal canvas to match, renders the entire content, and outputs a single image covering the full height.

Internally, it's not literally scrolling and stitching — the browser renders the full page in one pass at the computed height. This is faster and produces cleaner results than manual scroll-and-stitch approaches.

Playwright

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'full.png', fullPage: true });
await browser.close();

Puppeteer

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
await page.screenshot({ path: 'full.png', fullPage: true });
await browser.close();

Screenshot API

curl -X POST https://api.nightglass.xyz/api/v1/screenshot \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"url": "https://example.com", "fullPage": true}' \
  --output fullpage.png

The lazy loading problem

This is the most common gotcha with full-page screenshots. Modern sites lazy-load images — they only fetch images when the user scrolls them into view. In a full-page capture, images below the initial viewport never enter the viewport (the browser renders the full height in one pass), so they remain unloaded.

The fix: scroll the page before capturing to trigger lazy loading:

// Scroll the page to trigger lazy loading
await page.evaluate(async () => {
  await new Promise((resolve) => {
    let totalHeight = 0;
    const distance = 500;
    const timer = setInterval(() => {
      window.scrollBy(0, distance);
      totalHeight += distance;
      if (totalHeight >= document.body.scrollHeight) {
        clearInterval(timer);
        window.scrollTo(0, 0); // Scroll back to top
        resolve();
      }
    }, 100);
  });
});

// Now all images are loaded — capture
await page.screenshot({ path: 'full.png', fullPage: true });

Infinite scroll pages

Some pages load more content as you scroll (social feeds, search results). Full-page capture of these pages will either capture only the initially loaded content or scroll forever. Set a maximum scroll height and accept that you'll capture a finite portion:

const MAX_HEIGHT = 10000; // Cap at 10,000px

await page.evaluate(async (maxH) => {
  await new Promise((resolve) => {
    let totalHeight = 0;
    const timer = setInterval(() => {
      window.scrollBy(0, 500);
      totalHeight += 500;
      if (totalHeight >= maxH || totalHeight >= document.body.scrollHeight) {
        clearInterval(timer);
        window.scrollTo(0, 0);
        resolve();
      }
    }, 200);
  });
}, MAX_HEIGHT);

Fixed headers and footers

Pages with position: fixed or position: sticky headers will render the header at every scroll position in a full-page capture, creating duplicated headers throughout the image. Some browsers handle this well, others don't.

Workaround: inject CSS to change fixed elements to relative or absolute before capturing.

await page.addStyleTag({
  content: '* { position: static !important; }'
});

This is aggressive — it'll break some layouts. A more targeted approach is to hide specific fixed elements by selector.

File size considerations

Full-page images are large. A page that scrolls for 5000 pixels at 1440px wide at 2x DPR produces a 2880×10000 pixel image — potentially 10–30MB as PNG. Use JPEG for full-page captures unless you specifically need lossless quality. JPEG at quality 80 will be 5–10x smaller than PNG.

nightglass supports fullPage: true in the API request body, handling the scroll, lazy loading, and capture automatically. See the screenshot endpoint reference.

For step-by-step tutorials on full-page capture in each library: Playwright tutorial and Puppeteer guide. For batch processing many pages, the scale architecture guide covers queue patterns. Full-page captures are the foundation of visual change monitoring.