Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

The reliable pattern is simple: start a controlled browser, create a page with the viewport you need, navigate with an explicit URL, wait for the state your screenshot requires, save the viewport, full page, or a specific element, then close the browser. The example below uses Playwright because its page API covers navigation, URL waits, viewport control, masking, animation handling, and PNG, JPEG, or WebP output.

Install Playwright and create a minimal capture

Use a current Node.js project, install Playwright, and download at least one browser binary:

mkdir site-capture
cd site-capture
npm init -y
npm install -D playwright
npx playwright install chromium

Create capture.mjs:

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  viewport: { width: 1440, height: 900 },
  deviceScaleFactor: 1
});
const page = await context.newPage();

const response = await page.goto('https://example.com', {
  waitUntil: 'domcontentloaded',
  timeout: 30_000
});

if (!response) {
  throw new Error('The browser did not receive a navigation response');
}
if (response.status() >= 400) {
  throw new Error(`HTTP failure: ${response.status()} ${response.url()}`);
}

await page.screenshot({ path: 'screenshot.png', type: 'png' });
await browser.close();

Run it with node capture.mjs. Include a scheme such as https:// in every URL. A successful navigation and a successful HTTP response are separate checks: page.goto can return a response for a 404 or 500 page without throwing, so inspect response.status() when those pages should fail your job.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose when navigation is complete

The right wait depends on how the page is reached and how it renders.

Direct URL navigation

page.goto supports lifecycle waits such as domcontentloaded. For pages that continue fetching data after the initial document, wait for the visible application state you actually need instead of assuming the first lifecycle event is sufficient.

await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
await page.locator('[data-testid="dashboard-ready"]').waitFor({ state: 'visible', timeout: 20_000 });
await page.screenshot({ path: 'dashboard.png', fullPage: true });

Navigation caused by a click

When an interaction changes the main-frame URL, wait for that URL explicitly. This avoids racing the screenshot against the click’s navigation.

await page.goto('https://example.com');
await Promise.all([
  page.waitForURL('**/account', { waitUntil: 'domcontentloaded' }),
  page.getByRole('link', { name: 'Account' }).click()
]);
await page.screenshot({ path: 'account.png' });

Network idle and fixed delays

A selector wait is usually more meaningful than a fixed sleep. Use a delay only for a known animation or an application with no dependable readiness marker. Network-idle waits can remain open on pages with analytics, streams, or long polling, so use them with a bounded timeout and a page-specific readiness check.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.goto('https://example.com/report', { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {});
await page.locator('main.report').waitFor({ state: 'visible' });
await page.waitForTimeout(300);

Capture the scope you need

Viewport screenshot

The default captures only what is visible in the current viewport:

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option
await page.screenshot({ path: 'viewport.webp', type: 'webp', quality: 85 });

Full scrollable page

Set fullPage: true to capture the page’s full scrollable height. Very long pages can create large images; consider element captures or a deliberate viewport if the result is used in a visual test.

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

One element

Capture a locator when headers, cards, charts, or components need independent records:

await page.locator('article.product-card').first().screenshot({
  path: 'product-card.png',
  animations: 'disabled'
});

Bytes instead of a file

Omit path to receive a byte buffer for hashing, diffing, object storage, or an HTTP response:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const pngBytes = await page.screenshot({ type: 'png' });
console.log(`Captured ${pngBytes.length} bytes`);

Make output deterministic

Viewport, device scale, and format

Set the viewport before navigation when responsive layout matters. A phone-sized viewport can expose site behavior that differs from desktop; configure it on the browser context rather than changing it after the page has rendered.

Rank #3
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
const context = await browser.newContext({
  viewport: { width: 390, height: 844 },
  deviceScaleFactor: 2,
  colorScheme: 'dark'
});

Playwright supports PNG, JPEG, and WebP. JPEG and WebP accept a quality value where supported. scale: 'css' produces one output pixel per CSS pixel; scale: 'device' uses device pixels and can make high-DPI files larger.

await page.screenshot({
  path: 'retina.webp',
  type: 'webp',
  quality: 80,
  scale: 'css'
});

Hide, mask, or disable moving content

For repeatable captures, hide timestamps, ads, rotating promos, or personal data and mask regions whose content must not be compared literally. Disable animations when the capture is a visual assertion.

await page.screenshot({
  path: 'stable.png',
  animations: 'disabled',
  mask: [page.locator('.live-price'), page.locator('.avatar')],
  maskColor: '#777777'
});

Wait for lazy images

Full-page captures can trigger lazy loading as the page is measured, but an application-specific check is safer:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.locator('img.hero').waitFor({ state: 'visible' });
await page.evaluate(() => Promise.all(
  [...document.images].map(img => img.complete
    ? Promise.resolve()
    : new Promise(resolve => { img.addEventListener('load', resolve, { once: true }); img.addEventListener('error', resolve, { once: true }); }))
));

Inspect structure separately

A screenshot records pixels, not accessible structure or the best way to discover controls. Use locators, accessibility snapshots, or page inspection for interaction and semantics; reserve screenshots for visual evidence.

Navigate authenticated and stateful pages

Use a browser context for isolated cookies, local storage, viewport, timezone, and geolocation. For repeated jobs, save and load a storage state rather than logging in for every URL. Treat authentication files as secrets and keep them out of source control.

const context = await browser.newContext({
  storageState: 'auth.json',
  timezoneId: 'America/New_York',
  locale: 'en-US',
  geolocation: { latitude: 40.7128, longitude: -74.0060 },
  permissions: ['geolocation']
});

Set custom headers, cookies, a user agent, or authorization only when the target permits it and your automation is authorized. Do not attempt to bypass access controls, CAPTCHAs, or terms that prohibit automated access.

Handle failures and diagnose bad screenshots

  • “Invalid URL” or immediate navigation failure: pass a complete URL such as https://example.com, check for accidental whitespace, and verify DNS and network access from the machine running the browser.
  • 404 or 500 image saved: inspect the Response returned by goto; navigation success does not mean an HTTP success.
  • Timeout waiting for a selector: confirm the selector in the same viewport and account for iframes, authentication, consent dialogs, or a changed application state. Increase the timeout only after fixing the readiness condition.
  • Click races with navigation: pair the click and waitForURL in Promise.all, as shown above.
  • Blank or partially rendered capture: wait for a meaningful application selector, image completion, or a bounded network-idle period; verify that scripts were not blocked.
  • Different pixels on different machines: browser version, operating system, fonts, hardware, power state, headless mode, color settings, and device scale can all change rendering. Pin the browser/runtime used by visual tests and compare in a consistent environment.
  • Huge files or memory pressure: prefer WebP or JPEG where lossless PNG is unnecessary, use CSS scale, capture an element, or split an extremely long page. Close each context and browser in a finally block.
let browser;
try {
  browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com', { timeout: 30_000 });
  await page.screenshot({ path: 'result.png' });
} finally {
  await browser?.close();
}

Use screenshots in visual tests

For a one-off record, page.screenshot is enough. For regression testing, Playwright Test’s toHaveScreenshot waits for consecutive screenshots to stabilize before comparing them. Keep browser, operating-system, fonts, viewport, scale, and headless settings consistent; otherwise a visual difference may describe the environment rather than your code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { test, expect } from '@playwright/test';

test('home page stays visually stable', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('home.png', {
    fullPage: true,
    animations: 'disabled'
  });
});
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or a PDF, while the service accepts the page as a visitor: cookie and consent banners are accepted and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before capture. Each cleanup step can be turned off.

Only clean shots are billed. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and whether it was billed. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools to Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all options. This one-call example captures Stripe:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.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://stripe.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://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
await Bun.write('shot.webp', res);

ScreenshotNeo also supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, 12 device presets or any viewport, retina scale, PDF paper and page controls, custom CSS and JavaScript, pre-capture clicks, hidden selectors, waits for selectors, delays or network idle, request/resource blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, an OpenAPI specification, and familiar parameter names for easier migration.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; Growth is $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is available on every plan. Sign up free to get the 1,000 monthly screenshots without a card.

Choosing between browser automation and an API

Need Best fit Reason
Complex clicks, custom application logic, or in-process assertions Playwright You control the browser, context, locators, waits, and test runner.
One-off or scheduled URL captures without browser maintenance ScreenshotNeo A single HTTP call handles rendering and cleanup, with verdict and billing headers.
AI-agent screenshot and page inspection workflows ScreenshotNeo MCP Use take_screenshot, get_page_info, and capture_pdf from an MCP client.
Large batches Either, depending on control needs Run isolated Playwright contexts or use ScreenshotNeo bulk capture for up to 100 URLs per call.

Frequently Asked Questions

Does a screenshot prove that a page is accessible or correct?

No. It records rendered pixels. Check HTTP status, application readiness, accessibility structure, and any business assertions separately.

Should I use fullPage for every capture?

No. Use viewport captures for the visible state, element captures for components, and fullPage only when the complete scrollable document is the evidence you need.

Why do two visually identical runs differ by a few pixels?

Rendering can vary with browser and operating-system versions, fonts, hardware, headless mode, color settings, power state, and device scale. Standardize those inputs before investigating the page.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.