How to Capture Website Thumbnails with Playwright and Save Them as PNG
Capture a website thumbnail as a PNG with Playwright. Learn how to set the viewport, control output dimensions, handle dynamic pages, and fix common issues.
Use Playwright’s page.screenshot() method with a path ending in .png. The extension selects PNG, which is also the documented default format. For a thumbnail, set a deliberate viewport and leave fullPage off so the image shows the visible browser area.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1200, height: 630 },
deviceScaleFactor: 1,
});
await page.goto('https://example.com', { waitUntil: 'load' });
await page.screenshot({ path: 'thumbnail.png' });
} finally {
await browser.close();
}
})();
The 1200 × 630 viewport is an example composition, not a Playwright requirement. Set dimensions for the surface where the thumbnail will appear. The saved file is written relative to the process’s current working directory. See the official Playwright Page API and screenshots guide.
1. Install Playwright and prepare the browser
In a new Node.js project, install Playwright and its browser binaries:
npm init -y
npm install playwright
npx playwright install chromium
Save the example as capture.js and run node capture.js. The script launches Chromium, creates a page with a fixed viewport, navigates, writes the PNG, and closes the browser even if navigation or capture fails.
If Chromium is already installed in your environment, the install command may be unnecessary. In containers or fresh machines, installing the package alone may not install the browser executable.
2. Choose viewport size, page area, and scale
Viewport or full page
By default, Playwright captures the viewport. This is usually what you want for a thumbnail with a fixed aspect ratio. Set fullPage: true to capture the full scrollable document; the resulting image can be much taller and may no longer fit a thumbnail slot.
// The visible viewport (default)
await page.screenshot({ path: 'thumbnail.png' });
// The entire scrollable page
await page.screenshot({ path: 'full-page.png', fullPage: true });
CSS pixels versus device pixels
The screenshot option scale accepts 'css' or 'device'. Device scale is the default and produces output in device pixels, which can be larger than the CSS viewport on high-DPI configurations. Use scale: 'css' for one output pixel per CSS pixel. Set the browser context’s deviceScaleFactor deliberately when exact output dimensions matter; its default is 1. See the BrowserType API.
await page.screenshot({
path: 'thumbnail.png',
scale: 'css',
});
For predictable dimensions, define both viewport and device scale and keep them fixed between runs. With a 1200 × 630 viewport and device scale factor 1, the intended output is 1200 × 630 pixels. A higher device scale factor increases pixel dimensions; account for that in downstream storage and display.
Clip a rectangle or capture one element
Use clip to capture a rectangle of the page, or call screenshot on a locator when the thumbnail should show a particular element.
// Rectangle in page coordinates
await page.screenshot({
path: 'region.png',
clip: { x: 0, y: 0, width: 900, height: 500 },
});
// A specific element
await page.locator('main article').screenshot({ path: 'article.png' });
The selected element must exist and be visible for a useful capture. If the selector matches nothing or matches an unexpected element, wait for the right content and verify the selector.
3. Wait for the page and make captures repeatable
A successful navigation does not guarantee that every image, font, or client-rendered component is ready. Choose a navigation condition that matches the site, then wait for a specific element or a short, justified delay if the page fills in after navigation.
await page.goto('https://example.com', { waitUntil: 'load' });
await page.locator('main').waitFor({ state: 'visible' });
await page.screenshot({ path: 'thumbnail.png' });
For stable repeat captures, remove sources of variation where practical. Playwright supports disabling animations, hiding the caret, masking locator bounds, and applying a stylesheet during screenshot capture. These options change or cover page content, so apply them only when that is appropriate for the thumbnail.
await page.screenshot({
path: 'thumbnail.png',
animations: 'disabled',
caret: 'hide',
style: '*, *::before, *::after { transition: none !important; }',
});
For visual baselines, rendering can vary with the host operating system, browser version, settings, hardware, power source, and headless mode. Keep the capture environment consistent when comparing images. The Playwright visual comparisons guide discusses these sources of variation. Screenshot assertions such as toHaveScreenshot() are a separate Playwright Test workflow; for a simple PNG file, use page.screenshot().
4. Save the image to a known location
With path omitted, page.screenshot() returns image bytes instead of writing a file. Relative paths resolve from the process working directory, which may differ from the script’s directory.
const path = require('node:path');
const output = path.resolve(process.cwd(), 'thumbnail.png');
await page.screenshot({ path: output });
console.log(`Saved ${output}`);
You can also save the returned buffer yourself, which is useful when another part of the program handles storage:
const fs = require('node:fs/promises');
const png = await page.screenshot();
await fs.writeFile('thumbnail.png', png);
For PNG, a quality setting does not apply. Quality controls are for JPEG and WebP behavior; PNG is the appropriate choice when lossless output is needed.
5. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a screenshot or PDF. Its API documentation describes the request and 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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = require('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, no card required.
6. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| No file appears | No path was supplied, so the method returned a buffer. |
Pass a path ending in .png, or write the returned buffer to a file. |
| File is in an unexpected directory | The relative path is based on the process current working directory. | Log process.cwd() or use path.resolve() with a deliberate output directory. |
| Image is far taller than expected | fullPage: true captures the full scrollable page. |
Omit it or set fullPage: false for a viewport thumbnail. |
| Pixel dimensions are larger than the viewport | The screenshot uses device scale by default, and high-DPI settings can multiply output pixels. | Use scale: 'css' and set the context deviceScaleFactor deliberately. |
| PNG quality setting has no effect | PNG does not use the screenshot quality option. | Keep PNG for lossless output; use JPEG or WebP when lossy compression is desired and supported by the API. |
| Images or page content are missing | Capture occurred before page-specific content finished rendering, or lazy content had not loaded. | Wait for a visible content locator or an appropriate readiness condition before capture. |
| Output changes across runs | Dynamic elements or differences in browser and host rendering affect the pixels. | Control animations and dynamic content where appropriate, and keep the capture environment consistent. |
| Browser launch fails | The expected Chromium executable may not be installed in a fresh environment. | Run npx playwright install chromium and check that the runtime has the required browser dependencies. |
7. Performance, reliability, and cost
Each capture requires a browser page and a navigation, so total time depends on the target site and when its content becomes ready. Reuse a browser process for batches of URLs, create and close pages deliberately, and always close the browser in a finally block. Set navigation timeouts and handle failures in production so one inaccessible site does not stall an entire batch.
For reliability, define the viewport, device scale, browser version, and readiness condition as part of the capture configuration. Treat third-party pages as variable: network failures, bot checks, slow resources, and changing content can all affect the result. PNG can use more storage than lossy formats; keep the format and pixel dimensions aligned with the thumbnail’s use.
Playwright is an open-source browser automation library; this workflow runs in your own environment, so account for the compute, browser installation, storage, and maintenance required by your deployment. There is no specific performance benchmark established by the cited documentation, so measure representative pages in the environment where captures will run.
FAQ
Does Playwright save PNG by default?
PNG is the screenshot API’s documented default, and a .png path makes the intended format explicit.
Can I make the image exactly thumbnail-sized?
Set the viewport to the desired CSS dimensions and control device scale. Use scale: 'css' when you want one output pixel per CSS pixel.
Should I use fullPage for a thumbnail?
Usually not when the destination expects a fixed aspect ratio. Use it when the complete document is the image you need.
Why use toHaveScreenshot() instead?
Use it for Playwright Test screenshot assertions and visual comparisons. For simply writing a screenshot file, page.screenshot() is the direct API.


