Guide
How to screenshot a website programmatically
A viewport screenshot is three lines of Puppeteer. A full-page screenshot that looks like the page is where the work starts. This guide compares the three ways to do it and walks through every gotcha.
By Roger Campos · Last updated: September 2026
TL;DR
To screenshot a website from code, drive a headless browser with Puppeteer or Playwright, or call a screenshot API. Your own browser gives full control and costs you the infrastructure; an API is one HTTP call. Either way, full-page captures need work: scroll to trigger lazy images, deal with sticky headers and cookie banners, and stay under Chrome's 16,384 px and WebP's 16,383 px limits.
Free plan, no credit card. 1,000 credits a month.
The options
Three ways to screenshot a website from code
Every approach ends with a real browser rendering the page and encoding what it drew. The difference is whose browser it is, and who deals with it when it misbehaves.
| Puppeteer | Playwright | A screenshot API | |
|---|---|---|---|
| What it is | Node library driving Chrome, and Firefox over WebDriver BiDi | Library for Node, Python, Java and .NET driving Chromium, Firefox and WebKit | An HTTP endpoint: send a URL, get an image |
| Runs where | Your machine or server | Your machine or server | The provider's browsers |
| Control | Total | Total | What the API exposes |
| You maintain | Chrome binaries, memory, crashes, fonts, scaling | Same, times three engines | An API key |
| Cost | Your servers and your time | Your servers and your time | Per screenshot |
| Best for | Chrome-only jobs in a Node stack | Cross-browser checks, tests, non-Node stacks | Screenshots as a feature, not as infrastructure |
Between the two libraries, for plain screenshots, the choice matters less than it seems: the code is almost the same and both drive the same Chrome. Pick Playwright if you need Safari's engine or already use it for tests; pick Puppeteer if you want the smaller dependency maintained by the Chrome team. The real decision is whether to run browsers at all.
The easy part
A viewport screenshot in a few lines
Both libraries take a screenshot of the visible viewport by default, and a full-page one with a flag. This is the code every tutorial shows — and it's correct, for pages that behave.
import puppeteer from "puppeteer";
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1350, height: 800 });
await page.goto("https://example.com", { waitUntil: "networkidle2", timeout: 30_000 });
await page.screenshot({ path: "page.png", fullPage: true });
await browser.close();import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1350, height: 800 } });
await page.goto("https://example.com", { waitUntil: "load", timeout: 30_000 });
await page.screenshot({ path: "page.png", fullPage: true });
await browser.close();Run it against a real marketing page, though, and you'll meet the problems below within a day. None of them is a bug in the library; they're the page doing what it was built to do for a human scrolling through it.
The hard part
Full-page gotchas, and how to handle each
Lazy-loaded images come out blank
Most images below the fold use loading="lazy" or an IntersectionObserver: they only load when scrolled into view. A full-page capture doesn't scroll, so they're grey boxes or missing. Scroll through the page first, then wait for the images to finish.
await page.evaluate(async () => {
for (let y = 0; y < document.body.scrollHeight; y += window.innerHeight / 2) {
window.scrollTo(0, y);
await new Promise((r) => setTimeout(r, 150));
}
window.scrollTo(0, 0);
await Promise.all([...document.images].filter((img) => !img.complete)
.map((img) => new Promise((r) => { img.onload = img.onerror = r; })));
await document.fonts.ready;
});Sticky headers, and things sized to the screen
A fixed or sticky header is fine in a single-pass capture, but any tool that stitches a tall image from several scroll positions draws it once per tile. And some full-page methods work by resizing the viewport to the page's full height, which makes anything sized in vh — a hero set to 100vh — grow to thousands of pixels. If you see either, inject CSS before capturing: position: static on the header, a fixed height on the hero.
Cookie banners cover the page — and lock it
A headless browser is always a first-time visitor, so it always gets the consent banner. Worse, many consent managers set overflow: hidden on html or body while the banner is open, and a full-page capture of a page that can't scroll is exactly one viewport tall. Remove the banner's elements and restore overflow; the guide on cookie banners and ads covers how without clicking "accept".
Very tall pages come back truncated or empty
Chrome renders at most 16,384 device pixels in one surface. Past that, the capture doesn't fail — it comes back cut off, or empty. At a device scale factor of 2 that ceiling is 8,192 CSS pixels, which a long landing page passes easily. WebP is stricter still: the format stores each dimension in 14 bits, so 16,383 pixels is the most it can describe, and Chrome returns an empty string rather than an error past it. Clamp the height yourself, and check the output isn't empty.
"Loaded" isn't finished
networkidle waits for the network to go quiet, which some pages never do — analytics beacons, long-polling, video. load fires before a single-page app has fetched its data. The reliable signal is the element you care about: wait for its selector, then capture. Always set a timeout, and treat hitting it as a failure rather than screenshotting a spinner.
Running it
What running your own browsers costs
The library is free. The browser isn't. Each Chrome instance wants hundreds of megabytes of memory and a CPU core while it renders; a heavy page can take several seconds and much more. At any volume you need a pool, a queue, timeouts that actually kill hung tabs, restarts for leaked processes, fonts installed for every script you'll meet, and a plan for pages that fight headless browsers.
That work is worth doing when screenshots are your product, when you need a browser you control end to end, or when volume is high and steady enough to amortize it. It is usually not worth doing when screenshots are one feature among many — which is what screenshot APIs are for.
- Run your own if you need logged-in sessions, custom browser flags, other engines, or millions of captures a month with someone to own the fleet.
- Use an API if you want an image back from a URL and would rather not be paged when Chrome leaks memory at 3 a.m.
With URLpipe
The same screenshot as one HTTP call
URLpipe's /screenshot endpoint handles the gotchas above by default. It captures the full page unless you ask otherwise, scrolls through it first so lazy images are in the picture, uses a 1350 × 797 desktop window, and clamps tall pages to 16,384 pixels (one less for WebP). It costs 1 credit whatever options you use.
The options live in screenshot_options: viewport from 320 to 1920 wide, 1–3× scale, one element by selector, PNG, JPEG or WebP with a quality setting, dark mode, elements to hide and CSS to inject. What the page itself should wait for or lose — ads, cookie banners, a selector that appears late — goes in page_options. The response is the image in Base64, with an X-Result-Url header linking to it without a key for 30 days.
curl -X POST https://urlpipe.dev/screenshot \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/pricing",
"sync": true,
"screenshot_options": { "viewport_width": 390, "device_scale_factor": 2, "format": "webp" },
"page_options": { "block_cookie_banners": true, "block_ads": true }
}' | base64 --decode > pricing.webpWhat it doesn't do: log in, click through a flow before capturing, or run browsers other than Chrome. For those, Playwright is the right tool.
FAQ
Frequently asked questions
Should I use Puppeteer or Playwright for screenshots?
Why are images missing from my full-page screenshot?
Why does my full-page screenshot have the header repeated?
How tall can a screenshot be?
Is it cheaper to run my own headless Chrome?
Try it yourself
Free tools for this
No signup — run these on a real page right now, then call the same endpoint from your code.
- Screenshot any website from its URL
Paste a link and get a full-page PNG of the rendered page — JavaScript executed, exactly as a real browser would draw it. Great for previews, monitoring and visual QA.
Try it free - Take a full-page screenshot of any website
Paste a link and get the entire page in one PNG — every section down to the footer, not just what fits on the screen — with lazy-loaded images loaded before the capture.
Try it free - Screenshot a website the way a phone shows it
Paste a link and get a full-page capture at a 390 px wide viewport — a common phone width — so you see the site's mobile layout, not its desktop one.
Try it free
Put this into practice.
Each of the eight kinds of data URLpipe returns has a free, no-signup tool — try the ideas from this guide on a real page, then grab an API key to run them from your code. 1,000 credits a month, no card.