How to Capture Screenshots of Web Pages with Shadow DOM Content
Capture web pages with Shadow DOM using Playwright. Wait for components to render, then capture the viewport, full page, or a specific element.
To capture a web page that contains Shadow DOM content, take a screenshot of the browser-rendered page after the component has reached the state you want. With Playwright, use page.screenshot() for the visible viewport or full page, and a locator screenshot to capture one component. Playwright’s screenshot style option can also apply CSS through Shadow DOM and into inner frames.
Shadow DOM does not need to be flattened or rewritten for a screenshot: the browser paints the component as part of the page. The main practical challenges are waiting for asynchronous rendering and choosing a capture scope that includes the content.
Set up Playwright
The examples below use Node.js and Chromium. Install Playwright and its browser with:
npm install playwright
npx playwright install chromium
Save the following as capture-shadow-dom.cjs. Replace the example URL and my-component selector with the page and component you need to capture.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Wait for the custom-element host to appear in the rendered page.
const component = page.locator('my-component');
await component.waitFor({ state: 'visible', timeout: 15000 });
// Capture the visible viewport.
await page.screenshot({ path: 'viewport.png' });
// Capture the entire scrollable page.
await page.screenshot({ path: 'full-page.png', fullPage: true });
// Capture the component host and its rendered contents.
await component.screenshot({ path: 'component.png' });
} finally {
await browser.close();
}
})();
Run it with node capture-shadow-dom.cjs. A visible host does not always mean all of its asynchronous content is ready. When you know a stable condition for the desired state, wait for it before taking the screenshots, as described below. These calls follow the documented Playwright APIs; the sample URL and selector are placeholders, not a guarantee about any particular page.
Choose the screenshot scope
| Goal | Playwright call | What it captures |
|---|---|---|
| Visible page | page.screenshot({ path: 'page.png' }) |
The current viewport. |
| Whole page | page.screenshot({ path: 'page.png', fullPage: true }) |
The page’s full scrollable area. |
| One component or element | page.locator('my-component').screenshot({ path: 'component.png' }) |
The located element, brought into view before capture. |
A locator screenshot is useful when the component is the deliverable, such as a chart, product card, or embedded widget. For a scrollable element, Playwright captures only the portion currently scrolled into view; it does not automatically stitch all of that element’s internal scroll area into one image. Scroll the element to the position you need before capture, or capture the page if you need the surrounding context.
For stable selection, prefer a meaningful component tag, accessible role, or label over a positional selector such as div:nth-child(4). A locator must identify the host or visible target on the page. If your task requires selecting or styling an element inside a shadow tree, see the Shadow DOM-specific notes below.
Wait for the component’s intended state
Navigation completing is not the same as every custom element finishing its work. A component may render after JavaScript runs, fetch data, reveal content after interaction, or load images lazily. Wait for a condition that represents the state you want to preserve.
For a visible host, use a locator state wait:
const component = page.locator('my-component');
await component.waitFor({ state: 'visible', timeout: 15000 });
If a known piece of content appears after the component initializes, wait for that content instead. Playwright locators can pierce open Shadow DOM, so a locator for a known inner text or element can work across an open shadow root:
await page.getByText('Loaded report', { exact: true }).waitFor({
state: 'visible',
timeout: 15000,
});
await page.screenshot({ path: 'report.png' });
Replace the example text with a stable value on the page. Closed shadow roots and page-specific rendering behavior can limit what automation can locate; in that case, wait for a visible host or another observable page condition. Avoid arbitrary sleeps when a specific state can be observed. If the page has no usable state signal, a short delay is a fallback, but it may be either wasteful or too short:
await page.waitForTimeout(1000); // fallback only; tune for the page
Adjust capture output and stabilize the image
Playwright screenshot options let you control output format and visual state. For example, disable animations and use the screenshot-time style option to hide a blinking cursor or another unwanted decoration:
await page.screenshot({
path: 'stable.png',
fullPage: true,
animations: 'disabled',
style: `
my-component .caret,
my-component .transient-decoration {
visibility: hidden !important;
}
`,
});
The style option is documented to pierce Shadow DOM and apply in inner frames. Use selectors that match the page’s actual content. Other useful options include:
| Option | Use | Consideration |
|---|---|---|
type |
Choose PNG or JPEG output. | Use a file extension that matches the selected format. |
quality |
Set lossy image quality for JPEG. | It applies to JPEG, not lossless PNG. |
fullPage |
Capture the full scrollable page. | Very long pages produce large images and may expose lazy-loading behavior. |
clip |
Capture a defined rectangle. | Coordinates and dimensions must cover the intended content. |
scale |
Choose CSS-pixel or device-pixel output. | 'css' produces one output pixel per CSS pixel; 'device' can create larger files on high-DPI displays. |
animations |
Disable or fast-forward animations for capture. | Disabling animations changes the captured visual state; use it when a steady image is desired. |
mask |
Cover selected locators in the screenshot. | Useful when dynamic regions should be obscured consistently. |
style |
Apply temporary screenshot CSS. | Can reach Shadow DOM and inner frames; it does not modify the site’s source. |
Inspect the result at its intended display size. CSS-pixel output is often easier to compare across device scale factors, while device-pixel output retains more pixels on high-DPI displays and increases file size.
What Shadow DOM changes for screenshot capture
Shadow DOM organizes a web component’s internals and affects how page scripts and selectors reach them. A screenshot records the pixels the browser rendered, so a page-level screenshot includes visible component content without needing to export the shadow tree separately.
The distinction matters when choosing or modifying content. Playwright documents locators that can pierce open Shadow DOM, and its screenshot style option pierces Shadow DOM for temporary styling. Do not assume a selector can access every component: closed roots and site-specific behavior may prevent direct inspection. You can still capture what is visibly rendered by taking a page or host-element screenshot.
For Chrome tooling authors who need structural diagnostics rather than an image, the Chrome DevTools Protocol’s DOMSnapshot domain can return a flattened DOM representation that includes iframes and template contents, with Shadow DOM flattened in the returned tree. The Page protocol also exposes screenshot and snapshot commands. Most screenshot workflows can use Playwright’s higher-level APIs instead.
Capture a component after a user action
Some components display the desired content only after a click or other interaction. Perform the action, wait for the resulting state, and then capture:
const component = page.locator('my-component');
await component.waitFor({ state: 'visible' });
await component.getByRole('button', { name: 'Show details' }).click();
await component.getByText('Additional details', { exact: true }).waitFor({
state: 'visible',
});
await component.screenshot({ path: 'details.png' });
Use the actual accessible name and state text from the page. If the action opens a dialog or changes the page substantially, a page screenshot may be more appropriate than a component screenshot.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The component area is blank | The screenshot ran before the component rendered or its data arrived. | Wait for a visible host and, when possible, a specific content or state locator before capture. |
| The host is visible but internal content is missing | Rendering is asynchronous, the content depends on interaction, or the component has page-specific behavior. | Wait for the intended state, trigger the required action, and inspect the page in the same browser context. |
| An inner selector cannot find an element | The selector may not match the rendered structure; the root may be closed; or the element is not yet present. | Check the host selector, wait for content, and use a locator based on a stable accessible name or visible text when available. Capture the host or page if only pixels are needed. |
| The full-page image omits content lower down | Content may be lazy-loaded only when scrolled into view. | Scroll through the page and allow the relevant content to load before capturing, then inspect the output. |
| The element image cuts off part of a scrollable component | Locator screenshots include only the currently scrolled portion of a scrollable element. | Scroll the component to the required position before capture, or use a page-level capture if you need the surrounding page. |
| The image changes between runs | Animations, rotating content, timestamps, network data, or font/image loading may vary. | Wait for the intended state, disable animations when suitable, mask volatile regions, and use a consistent viewport and browser setup. |
| The image is unexpectedly large | The page is long or device-pixel scaling multiplies output dimensions. | Capture a component or clip, choose CSS-pixel scale, or use JPEG where lossy output is acceptable. |
| Navigation or capture times out | The page is slow, a request hangs, or the selected wait condition never occurs. | Use a navigation milestone suitable for the page, wait for a condition that can actually appear, and set a timeout that matches the workflow. Check authentication and network access. |
| Browser launch fails | The Playwright browser binary may not be installed for the project. | Run npx playwright install chromium and check that the runtime can launch Chromium. |
Performance, reliability, and cost
For repeated captures, reuse a browser process where your application architecture allows it, while creating an appropriate page or context for each independent session. This avoids paying browser startup work on every capture. Close pages and the browser when finished so resources are released.
Keep the capture scope as small as the output requires: an element image is usually less data than a very tall full-page image. Full-page capture can take longer and use more memory for long documents. CSS-pixel scaling can also reduce output size on high-DPI displays. Wait on a meaningful state instead of adding long fixed delays, but allow for the page’s actual network and rendering behavior.
Browser automation has no per-shot vendor charge, but it uses compute, memory, browser installation, and maintenance in the environment where it runs. Reliability depends on the target page, network, browser version, authentication, and dynamic content. Keep the browser version and viewport consistent when comparing captures, and review screenshots when a page changes. Playwright’s APIs provide capture controls, not a guarantee that every website renders identically across browsers or runs.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. A GET request with a URL returns an image or PDF; use the documented API options for output format and capture configuration. See the ScreenshotNeo API documentation for the full parameters.
For a page screenshot, one cURL request is:
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,
)
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}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify 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 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Do I need to convert Shadow DOM into regular HTML before taking a screenshot?
No. A browser screenshot captures the rendered pixels. Convert or rewrite the component only if another part of your workflow requires a different DOM representation.
Can I capture only the inside of a shadow root?
You can use a locator that reaches a target in an open shadow root and take an element screenshot. If you cannot reliably select the internal node, capture the visible component host instead.
Should I use a page screenshot or a locator screenshot?
Use a page screenshot when you need the viewport or full document. Use a locator screenshot when the component itself is the desired image and its visible bounds are sufficient.
Does a full-page screenshot include content that has not loaded yet?
No capture can include content that has not appeared. Wait for the relevant state and account for content that loads only after scrolling.


