How to capture a website screenshot with Playwright in TypeScript
Capture a website screenshot with Playwright in TypeScript. Save the viewport or full page, return a buffer, capture an element, and troubleshoot common failures.
Use Playwright’s page.screenshot() after navigating to the page. With Playwright Test and TypeScript, the smallest example is:
import { test } from '@playwright/test';
test('save a website screenshot', async ({ page }) => {
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png' });
});
This saves the current viewport as a PNG. Add fullPage: true to capture the scrollable page; omit path to receive the image bytes as a Node.js Buffer. Playwright’s Page screenshot API documents the available options.
1. Install Playwright and run the TypeScript example
For a new project, install Playwright Test and its browser binaries:
npm init playwright@latest
Follow the installer prompts and choose TypeScript. The generated project includes a playwright.config.ts and test directory. Put the example in tests/screenshot.spec.ts and run:
npx playwright test tests/screenshot.spec.ts
If you already have a TypeScript project and want the Playwright library without the test runner, install the package and Chromium:
npm install playwright
npx playwright install chromium
Playwright needs browser binaries compatible with the installed package. After updating Playwright, install browsers again if the expected executable is missing or mismatched. See the official browser installation guide.
2. Choose the capture area and output
Viewport screenshot
By default, page.screenshot() captures what is visible in the page viewport. Configure the viewport before navigating when you need a particular layout size:
import { chromium } from 'playwright';
async function main() {
const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'load' });
await page.screenshot({ path: 'viewport.png' });
} finally {
await browser.close();
}
}
main();
Full-page screenshot
Set fullPage: true to capture the page’s full scrollable document rather than only the current viewport:
await page.screenshot({ path: 'full-page.png', fullPage: true });
Very tall pages can create large images and consume substantial memory. If you only need a section, capture a locator or a clipped rectangle instead.
Capture one element
Use a locator’s screenshot method for a component such as a navigation bar, card, or chart. The locator must resolve to a visible element:
await page.locator('.header').screenshot({ path: 'header.png' });
Prefer a stable selector, such as a test id, over a class that changes with styling. A locator screenshot focuses the capture on that element and scrolls it into view as needed.
Capture a rectangle
Use clip when the desired area is a known rectangle in the page viewport. Its coordinates and dimensions are in CSS pixels:
await page.screenshot({
path: 'region.png',
clip: { x: 100, y: 80, width: 640, height: 360 }
});
Save to disk or return bytes
When path is present, Playwright writes the screenshot to that file. When the path is omitted, the call returns a Buffer, useful for uploading to object storage, attaching to a report, or passing to an image-processing library:
import { writeFile } from 'node:fs/promises';
const image: Buffer = await page.screenshot();
await writeFile('screenshot.png', image);
The method returns bytes even when a path is supplied. If you omit the path, choose the filename and format yourself when writing the bytes.
3. Configure image format, scale, and visual stability
| Option | What it controls | When to use it |
|---|---|---|
type |
png, jpeg, or webp |
PNG is the default; choose a compressed format when size matters. |
quality |
Compression quality for JPEG or WebP | Set a value from 0 to 100 for those formats; it has no effect on PNG. |
scale |
css or device pixel output |
css produces one output pixel per CSS pixel; device uses device pixel ratio and is the documented default. |
animations |
Whether finite animations are disabled and fast-forwarded during capture | Use disabled to reduce motion-related visual changes. |
caret |
Whether the text caret is hidden | Use hide for stable form-field images. |
mask |
Locators whose visible boxes are covered | Mask timestamps, avatars, or other dynamic regions. |
style |
CSS applied while taking the screenshot, including across shadow DOM | Hide or normalize elements without changing application source. |
omitBackground |
Whether to omit the default white background when supported | Useful for transparent output, typically with PNG. |
Example combining full-page output, WebP compression, and animation control:
await page.screenshot({
path: 'page.webp',
type: 'webp',
quality: 82,
fullPage: true,
animations: 'disabled',
caret: 'hide'
});
Use the official screenshot option reference for version-specific types and behavior. For visual regression comparisons, keep browser version, operating system, fonts, and execution environment consistent: rendering can vary across machines and browser settings. A saved screenshot is an artifact; expect(page).toHaveScreenshot() is an assertion that compares against a baseline. See Playwright visual comparisons.
4. Wait for the page you actually want to capture
Navigation completing does not guarantee that every image, animation, or client-rendered component is ready. Choose a deliberate readiness condition instead of adding a long arbitrary sleep:
await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
await page.getByRole('heading', { name: 'Dashboard' }).waitFor();
await page.screenshot({ path: 'dashboard.png', fullPage: true });
For a specific image or chart, wait for its locator or a page-specific ready signal. waitUntil: 'load' waits for the load event; domcontentloaded returns earlier. Network-idle conditions can be inappropriate for sites with long polling or persistent connections. A fixed delay can help with a known timed transition, but it makes the capture slower and is less reliable than waiting for a meaningful element.
Lazy-loaded content may only appear after scrolling. For full-page captures, inspect the resulting image; if the site loads sections only as they approach the viewport, scroll through the page before taking the final screenshot and wait for the content to appear.
5. Use the returned Buffer in a complete TypeScript script
This standalone script launches Chromium, navigates to a URL, waits for a heading, captures the full page as bytes, and writes those bytes to disk. Save it as screenshot.ts in a project configured for TypeScript and run it with your TypeScript runner:
import { chromium } from 'playwright';
import { writeFile } from 'node:fs/promises';
async function main(): Promise<void> {
const url = process.argv[2] ?? 'https://example.com';
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1
});
const response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30_000
});
if (!response || !response.ok()) {
throw new Error(`Navigation failed: ${response?.status() ?? 'no response'}`);
}
await page.locator('body').waitFor({ state: 'visible' });
const image = await page.screenshot({ fullPage: true, type: 'png' });
await writeFile('screenshot.png', image);
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
For production automation, validate the target URL and handle navigation errors according to your application’s needs. Always close the browser in a finally block so failed navigations or writes do not leave browser processes running.
6. Capture with cURL, Python, and Node.js using ScreenshotNeo
These are hosted API examples for cases where you do not want to install or operate a browser locally. Create an API key through ScreenshotNeo and keep it server-side; do not expose it in frontend code. The ScreenshotNeo API documentation lists the API options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
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()
with open("shot.webp", "wb") as f:
f.write(r.content)
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(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. Cookie banners are accepted like a visitor and removed along with known newsletter popups and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
See the API documentation for configuration and sign up free for 1,000 screenshots a month with no card.
7. Troubleshooting Playwright screenshots
| Symptom | Likely cause | Fix |
|---|---|---|
| Executable does not exist or browser launch fails | Browser binaries are absent or do not match the installed Playwright version. | Run npx playwright install chromium, or npx playwright install for the configured browsers. Repeat after updating Playwright. |
| Screenshot is blank or shows a loading shell | The capture happened before client rendering or the target content was ready. | Wait for a page-specific locator, response, or ready state before capturing. |
| Images are missing below the fold | Lazy images have not been requested because they were never scrolled into view. | Scroll through the page, wait for image loading, and capture after the content is rendered. |
| Element screenshot times out | The locator matched nothing, matched multiple unstable elements, or the element never became visible. | Use a unique stable locator and wait for the expected state. Check the selector against the page. |
| Cookie dialog or newsletter modal obscures the page | The site presented an overlay during navigation or after a delay. | Handle it as part of the flow: click an appropriate consent choice or close control, or use a test-specific style to hide it if that matches your goal. |
| Screenshot comparison differs on another machine | Fonts, browser build, OS rendering, device scale, or dynamic page data changed. | Run capture and comparison in a consistent environment; mask genuinely dynamic regions and control test data. |
| File extension does not match output bytes | The path suffix and explicit type option disagree. |
Keep the filename extension aligned with type. When writing a Buffer yourself, give it the correct extension. |
| Capture consumes too much memory or is slow | A very tall full-page image, device-pixel scaling, or expensive page rendering. | Capture only the needed locator or viewport, use scale: 'css' where suitable, or choose JPEG/WebP quality appropriate to the use. |
8. Performance, reliability, and cost considerations
- Reuse work carefully: browser startup is overhead. For a batch, launch one browser and create pages or contexts as needed; close each context and the browser after its work.
- Limit concurrency: many simultaneous full-page captures increase CPU and memory use. Set a bounded worker count and retry only transient failures.
- Keep captures deterministic: use a fixed viewport, device scale, browser version, locale, and test data for repeatable output. Disable motion or mask volatile fields when appropriate.
- Choose the smallest output that works: viewport captures and compressed formats reduce transfer and storage relative to huge full-page PNGs, with a quality tradeoff for lossy formats.
- Budget infrastructure: local Playwright has no per-shot ScreenshotNeo charge, but your execution still uses compute, memory, storage, and network capacity. Hosted capture can avoid local browser operations; ScreenshotNeo bills only clean shots and offers a free monthly plan plus paid tiers.
- Protect credentials and target access: keep API keys out of browser-delivered code. For local browser automation, only capture sites and authenticated states your process is authorized to access.
9. Frequently asked questions
Does Playwright take screenshots in TypeScript?
Yes. The TypeScript API is the same Playwright API used from JavaScript: navigate with page.goto(), then call page.screenshot().
How do I take a full-page screenshot?
Pass fullPage: true in the screenshot options. This captures the scrollable page rather than just the viewport.
Can I capture a screenshot without writing a file?
Yes. Omit path; the screenshot call returns a Buffer you can upload or process.
Can I use a screenshot as a visual test?
Yes. Playwright Test supports screenshot assertions such as toHaveScreenshot(), which compare against stored baselines. Use a consistent rendering environment.
When should I use an API instead of local Playwright?
Use a hosted API when you want to avoid managing browser installation and execution for screenshot jobs, or when an agent needs an MCP screenshot tool. Use Playwright when you need direct control of an existing test browser and its page state.


