How to Capture a Website Screenshot with Puppeteer in TypeScript
Capture a website screenshot in TypeScript with Puppeteer. Learn how to save viewport, full-page, and element captures, choose formats, and handle common issues.
Use Puppeteer’s page.screenshot() method after navigating to the page. Pass a path to save the image to disk; set fullPage: true to capture the full document, or use an element handle’s screenshot() method to capture one element. The example below uses TypeScript, closes the browser even if capture fails, and writes a PNG file.
1. Install Puppeteer and capture a page
Create a project and install Puppeteer. The package includes a compatible browser download as part of its normal installation.
mkdir puppeteer-shot
cd puppeteer-shot
npm init -y
npm install puppeteer
npm install --save-dev typescript tsx @types/node
Save this as screenshot.ts. Replace the URL and output path as needed.
import puppeteer from 'puppeteer';
async function main(): Promise<void> {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 60_000,
});
await page.screenshot({ path: 'screenshot.png' });
console.log('Saved screenshot.png');
} finally {
await browser.close();
}
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Run it with:
npx tsx screenshot.ts
page.screenshot() captures the current viewport by default. Supplying path saves the file relative to the process working directory. Without path, Puppeteer returns image bytes instead of writing a file.
2. Choose when the page is ready
Navigation completion and visual readiness are different things. Puppeteer’s documented screenshot example uses waitUntil: 'networkidle2', which waits for a period with little network activity. It is a useful starting point, not proof that every page has finished rendering: client-side apps can update after network activity settles, and pages with ongoing requests may never reach an idle state.
Choose a navigation condition that fits the target:
| Condition | Use it when | Watch for |
|---|---|---|
load |
You need the page load event, including dependent resources. | Some pages keep doing work after this event. |
domcontentloaded |
You need the parsed document and expect to wait for app content separately. | Images and other resources may still be loading. |
networkidle2 |
You want to wait for a short period with no more than two active network connections. | Background requests and live pages can make this unsuitable. |
networkidle0 |
You want to wait for a short period with no active network connections. | Analytics, polling, and streaming can prevent the condition. |
For an app that renders a known target after navigation, wait for that selector as well:
await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await page.waitForSelector('[data-testid="report"]', { timeout: 15_000 });
await page.screenshot({ path: 'report.png' });
If the site animates or loads data after the selector appears, wait for the relevant application state or a specific element to become visible. A fixed delay can be a last resort, but it adds time and may still be too short or unnecessarily long.
3. Capture the viewport, full page, a region, or an element
Viewport screenshot
The default captures the visible viewport. Set its dimensions before navigation or capture so responsive layout uses the intended viewport:
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('https://example.com');
await page.screenshot({ path: 'viewport.png' });
Full-page screenshot
fullPage defaults to false. Set it to true to capture the page’s full document rather than just the visible viewport:
await page.screenshot({ path: 'full-page.png', fullPage: true });
Long pages can produce large images and may stress browser or memory limits. Full-page capture does not guarantee that content which only loads after scrolling has appeared. If the page uses lazy-loaded images, scroll through it before capture and wait for the images or content you need.
One element
Wait for the selector, check that Puppeteer found it, then capture the element. The element screenshot method scrolls the target into view if necessary. It can fail if the element is detached from the document before capture.
const element = await page.waitForSelector('main article', { timeout: 15_000 });
if (!element) {
throw new Error('Target element was not found');
}
await element.screenshot({ path: 'article.png' });
A clipped rectangle
Use clip when you know the rectangle to capture in page coordinates. Its x and y identify the top-left corner; width and height set its size.
await page.screenshot({
path: 'region.png',
clip: { x: 80, y: 120, width: 640, height: 360 },
});
Use either fullPage or clip according to the capture you need. For a selector-based region, an element screenshot avoids having to calculate its coordinates yourself.
4. Set the output format and handle screenshot bytes
Puppeteer infers the image format from the path extension. PNG is the documented default. quality applies to formats that support it and does not apply to PNG. If you need bytes for an upload or another API, omit the path and use the returned Uint8Array.
const bytes: Uint8Array = await page.screenshot();
// Example: pass bytes to an API, or write them with your preferred file API.
Request base64 text when an interface specifically requires it:
const base64: string = await page.screenshot({ encoding: 'base64' });
Use omitBackground: true to omit the default white background and allow a transparent screenshot. Transparency depends on the selected image format; do not assume every format preserves it.
await page.screenshot({
path: 'transparent.png',
omitBackground: true,
});
5. Make a repeatable capture function
For scripts that capture multiple pages, create and close one browser process around the job, while using a new page for each capture. Set a viewport explicitly so output dimensions are predictable.
import puppeteer, { type Page } from 'puppeteer';
async function capture(page: Page, url: string, path: string): Promise<void> {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60_000 });
await page.screenshot({ path, fullPage: true });
}
async function main(): Promise<void> {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewportSize({ width: 1365, height: 900 });
await capture(page, 'https://example.com', 'example.png');
} finally {
await browser.close();
}
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
For production jobs, put a timeout around each navigation and capture, always close the browser in cleanup, and record the requested URL and failure stage. Avoid launching an unlimited number of browser processes at once; concurrent pages consume memory and CPU.
6. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser fails to launch | The browser executable is missing, incompatible with the installed package, or blocked by the runtime environment. | Install Puppeteer with its browser download, check the launch error, and configure the deployment environment to provide required browser dependencies. Use a compatible browser executable if your environment supplies one. |
| Navigation times out | The site is slow, unreachable, or never reaches the chosen idle condition because of ongoing requests. | Check URL reachability; increase timeout only when appropriate; use domcontentloaded and wait for the specific content you need instead of requiring network idle. |
| Screenshot is blank or incomplete | The app has not rendered its content, navigation failed, or a later client-side update is still pending. | Check the final URL and page content, wait for a meaningful selector or app state, and inspect navigation errors before capture. |
| Element selector times out | The selector is wrong, the element is in a frame or shadow root, or the element is only added after an interaction. | Verify the selector in the page, wait for the correct frame or state, or interact with the page before waiting for the element. |
| Element screenshot reports detached node | The page replaced or removed the element between locating it and taking the screenshot. | Wait for the page to settle, re-query the element immediately before capture, and avoid retaining handles across navigation or rerendering. |
| Output file is missing | The script failed before capture, the path is relative to a different working directory, or no path was supplied. |
Log the resolved output location, create the destination directory first if needed, and use an absolute path when the working directory may vary. |
| Image is unexpectedly large | A full-page capture covers a long document or the chosen format is lossless. | Capture only the needed viewport or element, and use a supported lossy format and quality setting when image fidelity requirements allow. |
7. Reliability, performance, and cost considerations
A screenshot job includes browser startup, navigation, page readiness, rendering, and image encoding. Reusing a browser across a bounded batch avoids repeated startup, while closing it after the batch releases its resources. Limit concurrency according to the memory and CPU available to the process. Full-page screenshots and high resolution increase output size and capture work.
For reliable jobs, use explicit timeouts, wait for page-specific readiness, handle navigation and selector errors, and close the browser in a finally block. A successful navigation does not mean the page is visually correct: sites can show bot checks, consent banners, or errors in the captured viewport. Review outputs for workflows where a bad image has downstream consequences.
Puppeteer is an open-source browser automation library; this method does not charge per screenshot through Puppeteer itself. Your costs come from the machine or browser infrastructure you run, plus the engineering and operations needed to maintain it. Hosted screenshot services trade browser setup and maintenance for a per-plan service cost.
8. Or skip the browser setup
If you want a screenshot without installing and operating Puppeteer, ScreenshotNeo provides a website screenshot API and MCP server. It returns PNG, JPEG, WebP, or PDF output. See the API documentation for the supported parameters and response details.
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
- Cookie banners are accepted and removed before the shot; known consent platforms, newsletter popups, and chat widgets can also be removed.
- Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing. Response headers say which page verdict applied and whether the request was billed.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents and MCP clients. - The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; all features are on every plan.
Sign up free for 1,000 screenshots a month, with no card required.
9. FAQ
Does Puppeteer save screenshots automatically?
No. Pass a path to save to disk. Otherwise, the call resolves to image bytes by default, or a base64 string when requested.
Can I capture just one DOM element?
Yes. Get an element handle and call its screenshot() method. Make sure the element remains attached until capture finishes.
Why does my screenshot look different from the browser window?
Puppeteer uses the viewport and rendering state of its browser page. Set viewport dimensions before navigation, and wait for the same content and state that the screenshot needs.
Should I use network idle for every page?
No. It is one readiness option. For pages with continuous requests or client-side rendering, waiting for a meaningful selector or app state is often more appropriate.


