How to Screenshot a Single Element with Playwright
Use Playwright’s locator.screenshot() to capture one element, control animations and output, and troubleshoot clipped or unstable images.
Use Playwright’s locator.screenshot() method. It finds one element, scrolls it into view, waits for actionability, clips the image to that element, and returns a Buffer. Add path to save the image. The default format is PNG.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.locator('.header').screenshot({ path: 'header.png' });
await browser.close();
See the Locator screenshot API and the official screenshot guide. Options can vary by installed Playwright version, so check the API reference for your project.
1. Install Playwright
npm install playwright
npx playwright install chromium
The browser install is required on a new machine or CI runner. If your project already uses Playwright Test, its configured browsers may already be available.
2. Choose a reliable locator
A locator identifies the single element to capture. Prefer stable, user-facing selectors over generated class names.
// CSS selector
const card = page.locator('[data-testid="pricing-card"]');
// Role and accessible name
const button = page.getByRole('button', { name: 'Buy now' });
// Text (use only when the text is stable)
const heading = page.getByText('Enterprise plan', { exact: true });
// Narrow a repeated component
const firstCard = page.locator('.pricing-card').first();
If a locator matches multiple elements, Playwright’s strictness rules can fail the operation. Narrow it with a more specific selector, filter(), first(), or nth(), depending on the intended target.
3. Save the image or process it in memory
Save directly to a file
await page.locator('.header').screenshot({ path: 'header.png' });
Use the returned Buffer
const image = await page.locator('.header').screenshot();
console.log(`Captured ${image.length} bytes`);
// Send image to storage, attach it to a test report, or process it in memory.
The output format is inferred from the filename extension when path is supplied. Without a path, the returned buffer uses PNG output by default.
4. Control format, quality, scale and background
await page.locator('.hero').screenshot({
path: 'hero.webp',
type: 'webp',
quality: 85,
scale: 'css',
omitBackground: true
});
| Option | Use it for |
|---|---|
type |
png, jpeg, or webp output. |
quality |
JPEG/WebP compression quality. It does not apply to PNG. |
scale: 'css' |
One output pixel per CSS pixel; smaller, predictable files. |
scale: 'device' |
Device-pixel output; sharper on high-DPI contexts but potentially larger. |
omitBackground |
Transparent background where supported. JPEG cannot preserve transparency. |
5. Make captures repeatable
Disable animations and transitions
await page.locator('.dashboard-card').screenshot({
path: 'dashboard-card.png',
animations: 'disabled'
});
With animations disabled, finite animations are fast-forwarded and infinite animations are canceled during capture, then resumed. This is useful for visual regression images. It does not freeze data that changes because of timers, network requests, or application state.
Inject capture-only CSS
await page.locator('.dashboard-card').screenshot({
path: 'dashboard-card.png',
style: `
.cursor, .live-indicator { visibility: hidden !important; }
.timestamp { opacity: 0 !important; }
`
});
Mask dynamic regions
await page.locator('.profile-card').screenshot({
path: 'profile-card.png',
mask: [page.locator('.avatar'), page.locator('.last-seen')],
maskColor: '#777'
});
Control the caret
await page.locator('textarea[name="message"]').screenshot({
path: 'message-box.png',
caret: 'hide'
});
Use these controls explicitly. A locator screenshot does not automatically remove cookie banners, chat widgets, sticky headers, or other overlays.
6. Wait for the element’s real state
locator.screenshot() performs actionability checks and scrolls the element into view. You still need to wait for application-specific readiness, such as data loading or a chart finishing rendering.
const chart = page.locator('[data-testid="sales-chart"]');
await chart.waitFor({ state: 'visible' });
await page.waitForFunction(() => window.salesChartReady === true);
await chart.screenshot({ path: 'sales-chart.png', animations: 'disabled' });
For a known delay, use a deliberate timeout sparingly:
await page.waitForTimeout(500);
await page.locator('.hero').screenshot({ path: 'hero.png' });
A selector or application-state wait is usually less flaky than a fixed delay.
7. Understand clipping, scrolling and occlusion
- The image is clipped to the matched element’s bounding box.
- If another element covers the target, the obstruction remains in the image.
- For a scrollable container, only the content currently visible in that container is captured; the method does not automatically stitch every scroll position into one image.
- If the element is detached from the DOM during capture, Playwright throws an error.
Capture a whole scrollable component
Element screenshots do not provide a full-scroll option for an arbitrary element. If you need every row in a scroll container, make the container expand temporarily or capture each scroll position and stitch the images yourself.
const list = page.locator('.virtualized-list');
await list.evaluate(el => {
el.style.height = `${el.scrollHeight}px`;
el.style.overflow = 'visible';
});
await list.screenshot({ path: 'full-list.png' });
This approach depends on the component. Virtualized lists may render only visible rows, so expanding the container alone may not create missing DOM nodes.
8. Complete runnable example
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1
});
const page = await context.newPage();
try {
await page.goto('https://example.com', {
waitUntil: 'networkidle',
timeout: 30_000
});
const target = page.getByRole('heading', { name: 'Example Domain' });
await target.waitFor({ state: 'visible' });
const buffer = await target.screenshot({
path: 'example-heading.webp',
type: 'webp',
quality: 85,
animations: 'disabled',
scale: 'css',
caret: 'hide'
});
console.log(`Saved ${buffer.length} bytes`);
} finally {
await browser.close();
}
9. Screenshot versus screenshot assertions
A normal locator screenshot captures and saves an image. A locator screenshot assertion waits for two consecutive screenshots to stabilize and compares the result with an expected snapshot. Assertions such as toHaveScreenshot() work with the Playwright test runner; they are not a replacement for a standalone capture script. See the Locator assertions reference.
import { test, expect } from '@playwright/test';
test('card matches its snapshot', async ({ page }) => {
await page.goto('https://example.com');
await expect(page.locator('.card')).toHaveScreenshot('card.png', {
animations: 'disabled'
});
});
10. Troubleshooting
| Error or symptom | Cause | Fix |
|---|---|---|
| Strict mode violation | The locator matches more than one element. | Use a unique attribute, role and name, filter(), or an intentional first()/nth(). |
| Timeout exceeded | The element never became actionable, the page is still loading, or the default timeout is too short. | Wait for the correct state, verify the selector, increase timeout only when justified, and inspect the page in headed mode. |
| Element is not attached to the DOM | A framework rerender replaced the node during capture. | Create the locator again after the state change and wait for the replacement element to be visible. |
| Cookie banner or chat bubble appears | Playwright captures what is visible; it does not remove overlays automatically. | Accept or hide the overlay explicitly, inject capture CSS, or use a capture service that handles consent and widgets. |
| Only part of a scrollable panel appears | Element screenshots contain the panel’s current viewport. | Expand the panel, capture scroll positions separately, or redesign the capture target. |
| Image changes between runs | Animations, blinking carets, timestamps, ads, random data, or fonts are changing. | Disable animations, hide dynamic selectors with style, mask regions, wait for stable data, and use fixed viewport/device settings. |
| JPEG transparency is missing | JPEG does not support transparent backgrounds. | Use PNG or WebP when transparency is required. |
| Browser executable missing | The Playwright browser binary is not installed on the machine or CI runner. | Run npx playwright install chromium and cache the browser in CI. |
The older elementHandle.screenshot() API is discouraged in current Playwright documentation. Prefer locator-based code so the element can be resolved at action time; see the ElementHandle reference.
11. Performance, reliability and cost
- Reuse a browser process and context for batches of captures instead of launching a new browser for every element.
- Use the smallest viewport and
scale: 'css'that meets your output requirements to reduce image size and encoding time. - PNG is lossless but often larger; WebP or JPEG can reduce transfer and storage size when their quality tradeoffs are acceptable.
- Waiting for
networkidlecan be slow or never settle on applications with long-lived connections. Prefer a specific readiness signal when possible. - Set explicit timeouts and always close pages, contexts, and browsers in a
finallyblock. - For visual tests, control fonts, viewport, device scale, animations, time, locale, and data so failures represent real visual changes.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. It can capture a page or a single element by CSS selector, while handling browser setup for you. Before capture it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.
Basic request:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo documentation for the element selector and other capture options. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Does locator.screenshot() capture the element’s entire content?
It captures the element’s visible bounding box. A scrollable element’s hidden content is not automatically included.
Can I return the image instead of writing a file?
Yes. Omit path; the method returns a Buffer that you can upload or process.
Why is my screenshot different on CI?
Differences commonly come from fonts, viewport size, device scale, animations, locale, time, or changing data. Fix those inputs before comparing images.
Should new code use ElementHandle screenshots?
No. Playwright marks that API as discouraged and recommends locator.screenshot().


