How to Capture a Screenshot of a Shadow DOM Element in Playwright
Capture an element inside an open Shadow DOM with Playwright, control animations and output format, and troubleshoot selectors, visibility, and closed roots.
To screenshot an element inside an open Shadow DOM in Playwright, locate it with a CSS selector that crosses the shadow boundary, then call locator.screenshot():
const target = page.locator('my-widget .target');
await target.screenshot({ path: 'element.png' });
Playwright CSS selectors pierce open shadow roots; XPath selectors do not. The element screenshot waits for actionability checks and scrolls the matched element into view. It captures the element’s visible rendered area, so overlays and the current scroll position of a nested scroll container can affect the result. [Playwright locator guidance] [Locator API]
1. Set up a runnable Playwright example
This TypeScript example opens a page containing a custom element, locates a descendant in its open shadow root, and saves the element screenshot. Install Playwright and its browser binaries in your project before running it.
npm install -D playwright
npx playwright install chromium
import { chromium } from 'playwright';
async function main() {
const browser = await chromium.launch();
const page = await browser.newPage();
try {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Replace these selectors with the custom element and descendant on your page.
const target = page.locator('my-widget .target');
await target.screenshot({ path: 'element.png' });
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The selector is an example pattern; the target page must actually contain my-widget with an element matching .target in its open shadow tree. Use the Playwright version installed in your project as the reference for supported options.
2. Choose a locator that crosses the shadow boundary
Playwright’s CSS locator engine pierces open shadow roots. A selector can start at the custom element host and continue into its shadow content:
const target = page.locator('my-widget .target');
await target.screenshot({ path: 'element.png' });
Prefer a role or text locator when it identifies the intended element clearly and reliably. CSS is appropriate when the component structure is the intended way to identify the target. XPath does not pierce shadow roots. [Playwright locator guidance]
Before capturing, make sure the locator resolves to exactly the intended element. If a component contains repeated matching descendants, scope the selector further or use a locator strategy tied to a stable role, accessible name, or component structure. An ambiguous or incorrect match can produce a valid screenshot of the wrong element.
3. Save the screenshot or use its buffer
locator.screenshot() returns an image buffer. Pass path to save it directly; omit path when you want to process, upload, or inspect the returned bytes in your own code. [Playwright Locator API]
const target = page.locator('my-widget .target');
const imageBuffer = await target.screenshot();
// Use imageBuffer with your application's file, upload, or image-processing code.
For a reproducible saved file, specify a path and a format using the type option. Playwright supports PNG, JPEG, and WebP for screenshots. If the path extension and requested type differ, set type explicitly so the output format is unambiguous.
await page.locator('my-widget .target').screenshot({
path: 'element.webp',
type: 'webp',
});
4. Control animation, masking, and output scale
Element screenshots accept options for output format and scale, animation handling, masking, and styling. These are useful when the page contains transitions or changing content. Check the Locator API documentation for the exact option types supported by your installed Playwright version. [Playwright Locator API]
| Option | Use | Practical note |
|---|---|---|
animations: 'disabled' |
Reduce differences caused by CSS animations, transitions, and Web Animations. | Use when the intended capture is a stable visual state. This changes animation behavior for the capture. |
mask |
Cover dynamic or sensitive regions in the screenshot. | Pass locators for the regions to mask; confirm the mask does not obscure content you need to inspect. |
type |
Choose PNG, JPEG, or WebP. | Set it explicitly when a specific output format matters. |
scale |
Choose CSS-pixel or device-pixel output scaling. | Choose based on whether you need a compact CSS-sized capture or device-pixel detail. |
style |
Apply a stylesheet during capture to control page styling. | Documented from Playwright v1.41; its injected stylesheet can pierce Shadow DOM and inner frames. |
await page.locator('my-widget .target').screenshot({
path: 'element.png',
animations: 'disabled',
mask: [page.locator('my-widget .personal-data')],
type: 'png',
scale: 'css',
});
The style option is useful when a page has dynamic content that can be hidden or adjusted for a controlled capture. Keep the stylesheet narrowly scoped so the capture continues to represent the target you mean to document. [Locator API]
await page.locator('my-widget .target').screenshot({
path: 'element.png',
style: `
my-widget .timestamp,
my-widget .rotating-promo {
visibility: hidden !important;
}
`,
});
5. Understand visibility, scrolling, and closed roots
- Actionability and scrolling: Playwright performs actionability checks and scrolls the element into view before capturing. A detached element causes an error. [Locator API]
- Occlusion: If another element covers the target, the covered portion will not actually be visible in the screenshot. Resolve the overlay or capture at a state where the target is unobstructed.
- Nested scrolling: The screenshot reflects the content currently visible in a scrollable container. Scroll that container to the intended position before taking the screenshot if the needed content is out of view.
- Open versus closed roots: CSS piercing is documented for open shadow roots. The cited locator guidance does not establish a general locator solution for reaching closed roots. If the component uses a closed root, use an exposed user-facing surface or another supported route provided by the component; do not assume a CSS locator can select its internals.
6. Or skip the browser setup
ScreenshotNeo offers a one-call screenshot API. It accepts a URL and returns an image or PDF; it captures a page URL rather than selecting an arbitrary node inside the page’s Shadow DOM.
ScreenshotNeo API documentation
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()
open("shot.webp", "wb").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}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use screenshot and page-info tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See ScreenshotNeo for the service and the API docs for request options.
Sign up for 1,000 free screenshots a month, no card required.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Locator finds no element | The host or descendant selector does not match the page, the component has not rendered yet, or the target is not in an open shadow root. | Check the host and descendant against the live page, wait for the component’s visible target, and confirm the root is open. |
| XPath selector does not reach the target | Playwright XPath selectors do not pierce shadow roots. | Use a CSS selector across the open shadow boundary or a suitable user-facing locator. [Locator guidance] |
| Screenshot errors because the element detached | The component rerendered or removed the matched node during actionability checks or capture. | Wait for the component to settle, then resolve the locator and capture again. Avoid retaining an element handle across a rerender when a locator can re-resolve it. |
| Image shows only part of the content | The target is clipped by a scrollable container, or the capture reflects the current scroll position. | Scroll the relevant container to the desired position before capture. An element screenshot captures the element’s rendered area; it is not a request to capture an entire scrollable document. |
| Target is missing or partially hidden | An overlay covers it, or the target is outside the visible area at capture time. | Dismiss or wait for the overlay, and ensure the target can be brought into view. |
| Output looks different between runs | Animations or dynamic content changed between captures. | Disable animations and consider masking dynamic regions or applying a controlled screenshot stylesheet. |
| Works on one Playwright version but not another | An option may not exist in the installed release; the screenshot style option is documented from v1.41. |
Check the installed Playwright version and its matching Locator API documentation before using newer options. |
8. Performance, reliability, and cost
An element capture is scoped to the matched element, which is useful when the full page is not needed. The operation still depends on launching or reusing a browser, navigating to the page, waiting for the target to be actionable, and rendering it. Reuse a browser process for multiple captures when appropriate, and keep selectors specific so the intended component can be resolved predictably.
For stable output, wait for the application state you need before capturing, disable animations where suitable, and account for overlays and nested scrolling. A changing or detached component can make a capture fail or produce inconsistent results. Playwright runs locally in your own browser environment, so there is no per-screenshot ScreenshotNeo charge for this DIY path; your compute and browser runtime have their own costs. ScreenshotNeo pricing is separate: 1,000 free monthly shots, then plans from $5 for 3,000, with each plan including every feature.
9. FAQ
Can Playwright screenshot a shadow-root descendant directly?
Yes, if it is reachable through an open shadow root. Select the descendant with a locator and call screenshot().
Does an element screenshot include the whole page?
No. It captures the area corresponding to the matched element. Use a page screenshot when the whole page is the subject.
Can I use this approach with a closed Shadow DOM?
The cited Playwright selector guidance documents CSS piercing for open roots and does not provide a general locator solution for closed roots.
Which format should I choose?
Use PNG for lossless output, or choose JPEG or WebP when those formats better suit your downstream workflow. Set type explicitly when format matters.
Sources
- Playwright Locator API: element screenshots, options, actionability, scrolling, and detachment behavior.
- Playwright locator guidance: CSS piercing of open shadow roots, XPath limitation, and locator recommendations.
- Playwright Locator API, Next: current-next screenshot API details; verify options against the installed release.


