How to Capture an Element Screenshot With an API
Capture one HTML element as PNG, JPEG, or WebP with Playwright, Puppeteer, or an API. Includes selectors, stabilization, errors, and production guidance.
To capture one HTML element, select it with a browser automation locator and call the element screenshot method. The browser calculates the element’s current bounds, scrolls it into view, and clips the output to those bounds.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.locator('.header').screenshot({ path: 'header.png', type: 'png' });
await browser.close();
For a hosted option, ScreenshotNeo can capture a CSS-selected element without you operating a browser. Its API accepts the selector and returns an image or PDF.
1. Capture an element with Playwright
Playwright’s locator screenshot is the recommended approach for new code. It waits for actionability checks, scrolls the element into view, and clips the result to the element’s size and position. You can save a file or keep the returned bytes in memory.
Save the element to a file
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1
});
await page.goto('https://example.com', { waitUntil: 'networkidle' });
const card = page.locator('[data-testid="pricing-card"]');
await card.waitFor({ state: 'visible' });
await card.screenshot({
path: 'pricing-card.png',
type: 'png',
animations: 'disabled'
});
await browser.close();
Return image bytes from an HTTP endpoint
import express from 'express';
import { chromium } from 'playwright';
const app = express();
const browser = await chromium.launch();
app.get('/element.png', async (req, res) => {
const page = await browser.newPage();
try {
await page.goto('https://example.com', { waitUntil: 'networkidle', timeout: 30000 });
const bytes = await page.locator('.header').screenshot({ type: 'png' });
res.type('png').send(bytes);
} catch (error) {
res.status(504).json({ error: error.message });
} finally {
await page.close();
}
});
app.listen(3000);
Omit path when you need the returned buffer. Set the response MIME type to match the format: image/png, image/jpeg, or image/webp. See the Playwright screenshot documentation for the current option names.
Choose a format and quality
const bytes = await page.locator('#invoice').screenshot({
type: 'jpeg',
quality: 82
});
PNG keeps text and sharp UI edges crisp. JPEG is usually smaller for photographic content. WebP is useful when your consumers support it and you want a compact image.
2. Select the right element
Use a stable selector that describes the element’s role or purpose. Data attributes and semantic selectors generally survive redesigns better than positional selectors.
// More stable
page.locator('[data-testid="invoice-total"]');
page.getByRole('button', { name: 'Download invoice' });
page.locator('#invoice-total');
// Fragile
page.locator('div:nth-child(4) > span');
If several nodes match, make the selection explicit:
const total = page.locator('[data-testid="invoice-total"]').first();
await total.screenshot({ path: 'total.png' });
For a selector supplied by a caller, validate or restrict it before passing it to the browser. A caller-controlled selector can target unintended content or create expensive queries.
3. Make the element visually stable
A screenshot captures the rendered state at one instant. Wait for the state that matters to your users rather than relying only on a fixed sleep.
Wait for a selector
await page.goto('https://example.com/dashboard');
await page.locator('[data-testid="chart"]').waitFor({ state: 'visible' });
await page.locator('[data-testid="chart"]').screenshot({ path: 'chart.png' });
Wait for application data
await page.goto('https://example.com/dashboard');
await page.waitForResponse(response =>
response.url().includes('/api/chart') && response.ok()
);
await page.locator('[data-testid="chart"]').screenshot({ path: 'chart.png' });
Disable animations
await page.addStyleTag({
content: `*, *::before, *::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}`
});
await page.locator('.hero').screenshot({ path: 'hero.png' });
Fonts, images, and client-side data can still load after navigation. Wait for a meaningful selector, a known response, or a page-specific readiness flag. A fixed delay is a fallback, not a guarantee.
4. Important element-screenshot behavior
- Covered pixels remain covered. If a modal, sticky header, or another element sits above the target, the screenshot reflects that visual stacking order. The browser does not reveal hidden pixels.
- Scrollable containers are clipped to their current view. Capturing a scrollable panel does not automatically stitch all of its off-screen content into one image.
- The element must exist at capture time. A missing locator times out. A detached element can fail while the page is re-rendering; locate it again after the update.
- Bounds are CSS-pixel based. Device scale factor controls how many physical pixels are produced. Use a higher scale when text must remain sharp in a retina asset.
- Transparent backgrounds depend on browser and page styling. If the page paints a background on an ancestor, omitting a background on the screenshot will not remove that painted content.
5. Puppeteer alternative
Puppeteer exposes ElementHandle.screenshot(). It scrolls the element into view and returns image data or writes a file. A detached handle throws, so reacquire the handle after rerenders.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
const element = await page.$('.header');
if (!element) {
throw new Error('Element .header was not found');
}
await element.screenshot({
path: 'header.png',
type: 'png'
});
await browser.close();
Use Puppeteer’s page-level clip option when you need a rectangle rather than a DOM node. Use fullPage only when the whole document is required. See the ElementHandle screenshot reference.
6. Element screenshot options to plan for
| Need | Approach |
|---|---|
| Sharp interface text | PNG plus an appropriate device scale factor |
| Smaller files | JPEG quality or WebP |
| Deterministic visual diffs | Disable animations, set viewport, wait for data and fonts |
| Hide secrets | Mask or hide sensitive selectors before capture |
| Capture a rectangle | Use a clip rectangle instead of an element method |
| All content in a scroll panel | Expand the panel or capture and stitch deliberate regions |
| Different user experience | Set cookies, headers, user agent, locale, timezone or geolocation before navigation |
Mask sensitive content
await page.locator('[data-sensitive]').evaluateAll(nodes => {
for (const node of nodes) {
node.textContent = '••••';
}
});
await page.locator('#account-card').screenshot({ path: 'account-card.png' });
Do this before the screenshot and avoid logging page HTML, cookies, authorization headers, or returned image bytes.
7. Or skip the browser setup
ScreenshotNeo accepts a URL and can target one element with a CSS selector. The same endpoint supports PNG, JPEG, WebP, full-page shots, device presets, retina scale, custom CSS and JavaScript, waits, hidden selectors, headers, cookies, user agents, geolocation, caching and other capture controls. Read the ScreenshotNeo API documentation for parameter names and response details.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
--data-urlencode selector=.pricing-card \
-o pricing-card.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://stripe.com",
"selector": ".pricing-card"
},
timeout=90
)
r.raise_for_status()
open("pricing-card.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
selector: '.pricing-card'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
Cookie and consent banners are accepted and removed before the shot, along with more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup 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. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
8. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Timeout waiting for locator | Selector is wrong, element is hidden, or the page never reaches the expected state | Check the selector, wait for a real readiness condition, and set a useful timeout |
| Element not found in Puppeteer | The selector matched nothing | Check the URL and rendered DOM; fail with a clear error instead of calling screenshot on null |
| Detached element error | A framework rerender replaced the node | Wait for the update, then locate the element again immediately before capture |
| Only part of a panel appears | The target is a scrollable container | Capture the visible state, expand the container, or capture separate regions |
| Popup covers the target | Another element is painted above it | Close the popup, hide its selector, or capture after the blocking state is gone |
| Text is blurry | Low device scale or a compressed format | Use PNG and a higher device scale; avoid repeated JPEG recompression |
| Images are missing | Lazy loading has not triggered or requests failed | Scroll the target into view, wait for image completion, and inspect network failures |
| ScreenshotNeo returns an unexpected verdict | The page is blank, blocked, timed out, cached, or failed to load | Inspect X-Page-Verdict and X-Billed, then adjust waits, headers, cookies or access requirements |
9. Performance, reliability and cost
- Reuse a browser process in a service, but create an isolated page or context per request.
- Set navigation and locator timeouts so one broken origin cannot occupy a worker indefinitely.
- Keep screenshots in memory when returning them directly; write to disk only when you need an artifact.
- Choose the smallest viewport and output format that meets the requirement.
- Cache deterministic captures and include URL, selector, viewport, scale, theme and relevant authentication state in the cache key.
- Close pages and contexts in a
finallyblock. Monitor memory when pages contain charts, videos or large canvases. - Use retries only for transient navigation failures. Repeating a deterministic selector failure wastes time and can duplicate work.
- For ScreenshotNeo, choose a cache TTL when repeated captures are acceptable. Cache hits are not billed, and failed loads and other non-clean outcomes are identified in the response headers.
10. Production checklist
- Use a stable selector and verify it matches the intended node.
- Set viewport, device scale, locale and theme explicitly.
- Wait for the element and for the data, fonts and images it needs.
- Disable animations when comparing images or generating documentation.
- Decide whether covered pixels and scrollable content are acceptable.
- Choose PNG, JPEG or WebP based on sharpness and size requirements.
- Mask secrets and keep credentials out of logs.
- Return the correct MIME type and a useful error status.
- Close browser resources and enforce timeouts.
11. FAQ
Can I capture an element without saving a file?
Yes. Playwright returns a buffer when path is omitted, and Puppeteer returns image data from its element handle method.
Does an element screenshot include content outside the element?
No. It is clipped to the element’s rendered bounds. A scrollable element shows its current scroll position.
Should I use a locator or an element handle?
Use a Playwright locator for new code. It can resolve the current node and includes the framework’s actionability behavior. Element handles are useful when you already hold a specific node but are more exposed to detachment.
Can I capture a cross-origin iframe element?
You cannot query arbitrary DOM inside a cross-origin frame from the parent page. Navigate or attach to the frame using the browser framework’s frame support, subject to the target site’s access rules.
What is the simplest hosted approach?
Use ScreenshotNeo’s shot endpoint with the target URL and selector. It handles browser execution and offers cleanup, waiting, output, caching and delivery options through the API.


