ScreenshotNeo

BlogHow-to

How to take a screenshot of a page with Puppeteer and TypeScript

Capture viewport, full-page, or element screenshots with Puppeteer and TypeScript. Includes runnable code, options, troubleshooting, and a no-browser API alternative.

By the ScreenshotNeo team4 October 202611 min read

Use Puppeteer’s page.screenshot() method to capture a page. Set path to save an image file, use fullPage: true for the full document, or call screenshot() on an element handle to capture one element. The example below uses TypeScript, sets a known viewport, waits for navigation, saves a PNG, and closes the browser even if capture fails.

Puppeteer’s official screenshots guide documents this workflow. Navigation readiness is only one part of deciding when a page is visually ready: pages that load content after navigation may need an additional wait for a selector or application-specific state.

1. Install Puppeteer and TypeScript

In a new project, install Puppeteer and the TypeScript tooling:

npm init -y
npm install puppeteer
npm install --save-dev typescript tsx @types/node

Puppeteer normally downloads a compatible browser during installation. Use the browser bundled for your Puppeteer version when possible: the launch options reference cautions that using a different executable path is at your own risk.

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Save the following as src/screenshot.ts and run it with npx tsx src/screenshot.ts.

2. Capture a page and save a screenshot

import puppeteer from 'puppeteer';

const url = process.argv[2] ?? 'https://example.com';
const outputPath = process.argv[3] ?? 'screenshot.png';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });

  const response = await page.goto(url, {
    waitUntil: 'networkidle2',
    timeout: 30_000,
  });

  if (!response) {
    throw new Error('Navigation did not return a main-resource response');
  }
  if (!response.ok()) {
    throw new Error(`Page returned HTTP ${response.status()}`);
  }

  await page.screenshot({ path: outputPath, type: 'png' });
  console.log(`Saved ${outputPath} (${response.status()})`);
} finally {
  await browser.close();
}

Run it with a target URL and optional output path:

npx tsx src/screenshot.ts https://example.com ./example.png

The browser launch, page creation, navigation, screenshot, and close sequence follows the Puppeteer guide. The try/finally ensures browser cleanup if navigation or capture throws. The Page.screenshot() API returns image bytes when no file path is supplied; when a path is set, it writes the file.

3. Choose what to capture

Viewport screenshot

Without fullPage, Puppeteer captures the current viewport. Set it before navigation for repeatable dimensions:

await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.screenshot({ path: 'viewport.png' });

The viewport defines the visible CSS pixel area. Device scale factor controls the output pixel density; a value of 2 produces a higher-density image for the same CSS viewport. It does not make text or assets sharper if the site serves unsuitable assets.

Full-page screenshot

Set fullPage: true to capture the full document rather than only the visible viewport:

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

Very tall pages can produce large images or encounter browser image-size limits. For exceptionally long content, capture sections with clip or use a PDF workflow if a paginated document is the desired output.

One element

Wait for the target, then use the element handle’s screenshot() method. Puppeteer attempts to scroll the element into view before capturing it, as described by the ElementHandle screenshot API.

const card = await page.waitForSelector('[data-testid="report-card"]', {
  visible: true,
  timeout: 10_000,
});

if (!card) throw new Error('Report card was not found');
await card.screenshot({ path: 'report-card.png' });

An element handle becomes invalid if the page removes or replaces that element. The API documents a detached-element error in this case. Wait for the page’s final state, re-query the element immediately before capture, or retry after a known client-side render completes.

Clip a region

Use clip to capture a rectangle in page coordinates. Specify a positive width and height:

await page.screenshot({
  path: 'region.png',
  clip: { x: 100, y: 200, width: 600, height: 400 },
});

Use an element screenshot when the region should follow an element’s measured position and dimensions. A fixed clip is useful when the coordinates are known and stable.

4. Wait until the page is ready

page.goto() supports navigation wait strategies. The Puppeteer guide demonstrates networkidle2; it is a choice, not a promise that all visual work is finished. Analytics, polling, and streaming requests can keep network activity open, while client-side rendering or delayed images can continue after network quiet.

Wait strategy Useful when Trade-off
domcontentloaded You will wait explicitly for the content you need. The page may still be loading styles, images, and scripts.
load The page relies on its normal load event. Some pages continue asynchronous work after load.
networkidle0 You need a period with no active network connections. Persistent connections can delay or prevent it.
networkidle2 You want network activity to settle while tolerating a small number of active connections. It still does not prove a client-rendered page is visually complete.

For a known application, wait for an application-specific selector or state:

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

For a simple fixed delay, use page.waitForTimeout() if supported by your installed Puppeteer types; otherwise use a Node timer. A delay is less reliable than waiting for a visible condition because it can be too short on slow runs and waste time on fast ones.

await new Promise((resolve) => setTimeout(resolve, 1_000));

Lazy-loaded images may only load as the page scrolls. For a full-page capture, consider scrolling through the document before the screenshot, then allow image decoding to settle:

await page.evaluate(async () => {
  const step = Math.max(400, window.innerHeight);
  for (let y = 0; y < document.body.scrollHeight; y += step) {
    window.scrollTo(0, y);
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  window.scrollTo(0, 0);
  await Promise.all(
    Array.from(document.images, (image) =>
      image.complete ? Promise.resolve() : new Promise<void>((resolve) => {
        image.addEventListener('load', () => resolve(), { once: true });
        image.addEventListener('error', () => resolve(), { once: true });
      }),
    ),
  );
});

This scroll technique is page-dependent: virtualized lists may replace off-screen content, and images can fail. If the site exposes a stable ready marker or an image-loading signal, wait for that instead.

5. Set format, quality, and background

The ScreenshotOptions reference documents these main choices:

Option Use
path Write the image to a file. The extension is used to infer image type when type is not specified.
type Select the supported image type for your Puppeteer version, such as PNG, JPEG, or WebP where supported.
quality Set lossy image quality from 0 to 100; it does not apply to PNG.
fullPage Capture the full document instead of the viewport.
clip Capture a specified rectangular region.
omitBackground Hide the default white background so transparent page areas can remain transparent.
encoding Request base64 text instead of the normal byte result when using the API return value.

Example of a JPEG screenshot with a chosen quality:

await page.screenshot({ path: 'page.jpg', type: 'jpeg', quality: 82 });

Example of a transparent PNG:

await page.screenshot({ path: 'transparent.png', type: 'png', omitBackground: true });

PNG is suitable for lossless output and transparency. JPEG is useful when smaller photographic images matter more than lossless edges or transparency. Quality does not affect PNG. Check the installed Puppeteer types for format and option availability when working with a different release; the documentation retrieved for this guide identifies version 25.12.0, which may differ from your installation.

6. Return image bytes instead of writing a file

Omit path to receive bytes, for example to upload the image or store it through another library. The API returns Promise<Uint8Array> normally, or a string when base64 encoding is requested.

import { writeFile } from 'node:fs/promises';

const image = await page.screenshot({ type: 'png' });
await writeFile('screenshot.png', image);

7. Reproducible dimensions and browser setup

Set the viewport explicitly when image dimensions matter. Puppeteer’s screen configuration guide says headless mode uses an 800 × 600 screen by default unless --window-size is specified, and documents --screen-info for custom layouts. The guide notes that --screen-info and dynamically adding or removing screens are headless-only. For ordinary page screenshots, setting the page viewport directly is usually the most relevant control.

Keep these sources of variation in mind:

  • Viewport width can change responsive breakpoints, line wrapping, and layout.
  • Device scale factor changes output pixel density.
  • Fonts may load differently across machines or containers; install the required fonts in the runtime if exact typography matters.
  • Animations, clocks, randomized content, and live data can change between runs. For stable output, use application fixtures or disable animations with page CSS when appropriate.
  • Browser and Puppeteer versions can change rendering. Puppeteer recommends its bundled browser for compatibility; executablePath is available, but using another browser is at your own risk.

8. cURL, Python, and Node.js alternatives

These approaches use the same general browser automation sequence: navigate, wait for the desired state, then capture. They are useful when the project language differs, but the TypeScript examples above remain the direct Puppeteer answer.

cURL: call a screenshot API

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp

Python with Playwright

Puppeteer is a Node.js library. For a Python browser-automation workflow, Playwright provides a comparable page screenshot operation:

pip install playwright
playwright install chromium
import asyncio
from pathlib import Path
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        try:
            page = await browser.new_page(viewport={"width": 1440, "height": 900})
            response = await page.goto("https://example.com", wait_until="domcontentloaded", timeout=30_000)
            if response is None or not response.ok:
                raise RuntimeError(f"Navigation failed: {None if response is None else response.status}")
            await page.screenshot(path="screenshot.png", full_page=True)
        finally:
            await browser.close()

asyncio.run(main())

Node.js with Puppeteer

Use the same TypeScript flow in JavaScript by removing the type annotations and saving as screenshot.mjs:

import puppeteer from 'puppeteer';

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

9. Or skip the browser setup

ScreenshotNeo provides a one-request screenshot API and an MCP server for developers and AI agents. See the ScreenshotNeo API documentation for 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)
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}`);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

10. Troubleshooting

Symptom Likely cause Fix
Browser executable missing The Puppeteer browser was not downloaded or the install cache is unavailable. Install Puppeteer successfully in the environment and use its bundled browser. Check deployment packaging and browser cache paths.
Navigation timeout The site is slow, never reaches the selected network-idle state, or has persistent requests. Use an appropriate timeout and a less restrictive navigation event, then wait for a specific selector that proves the content you need is ready.
Screenshot is blank or incomplete The capture happened before client rendering, a lazy image load, or a required interaction. Wait for an application-ready selector; scroll for lazy content; perform the necessary interaction before capture.
Element screenshot throws The element is absent, hidden, or detached between lookup and capture. Wait for a visible selector, locate it close to capture time, and handle page rerenders or retry after a stable state.
Image dimensions differ across runs Viewport, device scale, browser, fonts, or responsive layout differ. Set viewport and scale explicitly; standardize browser and fonts; control dynamic content.
Full-page capture is too large The document is exceptionally tall or contains large assets. Capture selected sections or a clip, reduce output dimensions where suitable, or create a PDF for paginated output.
Transparent areas appear white The default page background is included. Set omitBackground: true and use an output format that supports transparency, such as PNG.
Works locally but fails in a container Browser dependencies, fonts, or filesystem permissions differ in the deployment environment. Install the runtime dependencies required by the bundled browser, include fonts used by the page, and write to a permitted path.

11. Performance, reliability, and cost

Local Puppeteer has no per-screenshot API charge, but each worker needs browser CPU and memory, and the job takes as long as navigation, readiness waits, and rendering require. Reuse a browser process for multiple trusted jobs when practical, while isolating pages and ensuring each task closes its page. Always close the browser in a service shutdown path. Set explicit navigation and selector timeouts so a single slow page does not occupy a worker indefinitely.

For throughput, use a bounded queue and limit concurrent pages to the capacity of the machine. Full-page images use more memory than viewport captures. Avoid launching an unbounded number of browser processes, and record navigation status, elapsed time, capture dimensions, and failure reason to diagnose slow or flaky pages.

Reliability depends on the target site as well as your runtime: pages can block automation, show consent dialogs, return errors, or change layout. Check the main-resource response, wait for meaningful page state, retry only transient failures with a cap, and avoid treating a successful screenshot call as proof that the captured page was correct.

ScreenshotNeo pricing is Free for 1,000 shots per month, Starter $5 for 3,000, Growth $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. Every feature is on every plan. This can be useful when browser installation, cleanup of consent overlays, or per-job browser operations are more costly than an API request. See ScreenshotNeo for the product and plan details.

12. Frequently asked questions

Does Puppeteer need a visible desktop to take screenshots?

No. Puppeteer can run headlessly; the screenshot workflow does not require an interactive desktop.

Can I screenshot a page that requires login?

Yes, if your automation establishes an authorized session first. Use the site’s supported authentication flow and keep credentials out of source code and logs.

Can I use a system-installed Chrome?

You can configure an executable path, but Puppeteer documents that compatibility with a browser other than its bundled browser is your responsibility. For reproducible captures, use the expected bundled version when possible.

Why does networkidle2 still produce an unfinished page?

Network quiet does not mean application rendering is complete. Wait for the actual content or state that matters to the screenshot.

Can the screenshot method return bytes for an HTTP response?

Yes. Omit path, receive the screenshot bytes, and write or stream them using Node.js APIs. Use base64 encoding only when a text representation is needed.