ScreenshotNeo

BlogHow-to

How to Take a Screenshot of a Web Page with a Shadow DOM Element in Selenium

Locate a shadow host, search its shadow root, and capture either the component or the current browser view with Selenium.

By the ScreenshotNeo team4 October 20268 min read

To screenshot a Shadow DOM element with Selenium, find its custom-element host in the regular document, retrieve the host’s shadow root, find the target inside that root, then call the screenshot method on the target WebElement. Use a driver-level screenshot when you want the current browser view instead of a crop of one component.

The selectors below are examples; replace them with selectors from the page you are automating. Selenium’s API names differ slightly between language bindings and versions, so check the documentation for your installed binding. [WebElement API] [ShadowRoot API]

1. Find the host, then enter its shadow root

Shadow DOM introduces a separate search context. A document-level lookup can locate the custom-element host, but to find content inside its shadow tree, search from the host’s shadow root. For nested shadow components, repeat the host → shadow root → descendant steps at each boundary.

  1. Wait for the page and component to render.
  2. Locate the host in the ordinary document, such as my-widget.
  3. Retrieve its shadow root.
  4. Find the target descendant from that root.
  5. Capture the target element or the current browser view, depending on the desired scope.

2. Python: capture the target element

This example uses Selenium’s Python binding. It saves the target element as a PNG and closes the browser even if an error occurs.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

options = webdriver.ChromeOptions()
# For a headless run, uncomment the next line:
# options.add_argument("--headless=new")

driver = webdriver.Chrome(options=options)
try:
    driver.get("https://example.com")

    wait = WebDriverWait(driver, 10)
    host = wait.until(
        lambda d: d.find_element(By.CSS_SELECTOR, "my-widget")
    )
    shadow_root = host.shadow_root
    target = shadow_root.find_element(By.CSS_SELECTOR, ".target")

    # Saves the target element's screenshot as PNG.
    target.screenshot("target.png")

    # To capture the current browser view instead:
    # driver.save_screenshot("page.png")
finally:
    driver.quit()

WebElement.screenshot(filename) saves an element image as PNG in the Python API. The element API also exposes PNG bytes and base64 forms. [Selenium Python WebElement API]

Nested shadow roots in Python

For nested components, get each intermediate host from its parent shadow root before continuing:

outer_host = driver.find_element(By.CSS_SELECTOR, "outer-widget")
outer_root = outer_host.shadow_root
inner_host = outer_root.find_element(By.CSS_SELECTOR, "inner-widget")
inner_root = inner_host.shadow_root
target = inner_root.find_element(By.CSS_SELECTOR, ".target")
target.screenshot("nested-target.png")

3. JavaScript: capture and write the PNG

Selenium’s JavaScript WebElement takeScreenshot() resolves to base64-encoded PNG data. The following Node.js example writes that data to a file; it assumes your project has the selenium-webdriver package installed and that Chrome and its driver are available to Selenium.

const fs = require("node:fs/promises");
const { Builder, By } = require("selenium-webdriver");

(async () => {
  const driver = await new Builder().forBrowser("chrome").build();
  try {
    await driver.get("https://example.com");

    const host = await driver.findElement(By.css("my-widget"));
    const shadowRoot = await host.getShadowRoot();
    const target = await shadowRoot.findElement(By.css(".target"));

    const pngBase64 = await target.takeScreenshot();
    await fs.writeFile("target.png", Buffer.from(pngBase64, "base64"));

    // To capture the current browser view instead:
    // const pageBase64 = await driver.takeScreenshot();
    // await fs.writeFile("page.png", Buffer.from(pageBase64, "base64"));
  } finally {
    await driver.quit();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Selenium documents shadow-root descendant lookup separately from WebElement lookup. Its element screenshot captures the visible region encompassed by the element’s bounding rectangle. [ShadowRoot API] [WebElement API]

4. Choose the screenshot scope

What you need Call What it captures
One shadow-DOM component or descendant Python: target.screenshot(...); JavaScript: target.takeScreenshot() The element’s visible bounding region. Content that overflows or is clipped may not appear in full.
The current browser view Python: driver.save_screenshot(...); JavaScript: driver.takeScreenshot() The current browsing-context screenshot, rather than a crop limited to the target element.
The entire long document Check the selected browser and driver’s supported behavior. The reviewed Selenium sources do not establish a portable full-page method for this workflow.

Selenium’s guide shows driver and element screenshots as separate operations. Choose based on whether you need a component crop or the current view. [Selenium screenshot examples]

5. Wait for the component to be ready

Finding a host does not guarantee that its inner content is ready to capture. Many pages render custom elements after navigation or update their contents asynchronously. Use an explicit wait for the host, then wait for the target condition that matches your page.

from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

wait = WebDriverWait(driver, 15)
host = wait.until(EC.presence_of_element_located(
    (By.CSS_SELECTOR, "my-widget")
))
root = host.shadow_root

def target_is_present(_driver):
    try:
        return root.find_element(By.CSS_SELECTOR, ".target")
    except Exception:
        return False

target = wait.until(target_is_present)
target.screenshot("target.png")

This illustrates a wait for presence. If the page needs more time for visible content to settle, wait for an appropriate page-specific signal before taking the image. A fixed delay can be used when no better signal exists, but it may make runs slower or remain too short under load.

6. cURL, Python, and Node.js without browser setup

If your goal is a screenshot of a public page rather than Selenium interaction with a shadow-root descendant, ScreenshotNeo offers a one-request screenshot API. It captures a page URL and returns an image or PDF. See the ScreenshotNeo API documentation for request options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.write(r.content)

Node.js

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 bytes = Buffer.from(await res.arrayBuffer());
await require('node:fs/promises').writeFile('shot.webp', bytes);

ScreenshotNeo is a website screenshot API and MCP server for developers, made by Yorker Media. Its browser capture can accept cookie and consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses include X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

ScreenshotNeo has a free plan with 1,000 shots per month and no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is on every plan. That is useful when you need a page screenshot without maintaining browser and driver setup. It does not replace Selenium when your task depends on locating and capturing a specific shadow-DOM descendant.

Sign up free for 1,000 screenshots a month with no card.

7. Troubleshooting

Symptom Likely cause What to check
Shadow-root retrieval fails The selected element is not the shadow host, or it has no shadow root. Verify the host selector and confirm the component actually exposes a shadow root. Selenium documents failure when an element has no shadow root. [WebElement API]
Target lookup fails inside the root The selector is wrong, the target has not rendered, or another shadow boundary lies between the root and target. Check the selector, wait for rendering, and traverse each intermediate host’s shadow root. ShadowRoot lookup searches descendants in that root. [ShadowRoot API]
Screenshot is blank or incomplete The target may not be rendered, visible, or inside the visible bounding region; it may also be covered or clipped. Wait for the page-specific ready state, confirm the target’s geometry and visibility, and use a driver screenshot if you need the current view rather than an element crop.
Image is cropped unexpectedly An element screenshot captures the target’s visible bounding rectangle, not necessarily overflowing descendants. Use a driver-level screenshot for the current browser view, or adjust the page state and capture scope to suit the output.
Screenshot file is missing in remote execution The code may save on the remote process or worker rather than your local machine. Check where the binding writes screenshot output. Selenium can return screenshot data as well; transfer or write those bytes where the file is needed. [Python WebElement API]
Full-page image is shorter than expected Driver behavior for full-document screenshots is not established as portable in the reviewed sources. Confirm support for your exact browser and driver before relying on full-page capture.

8. Performance, reliability, and cost

  • Wait on a signal: explicit waits avoid both capturing before the component renders and always paying the delay of a long fixed sleep.
  • Capture only what you need: a component screenshot is a narrower output than a browser-view screenshot, but it does not include content outside the element’s visible bounds.
  • Close the driver: use a finally block so browser processes are cleaned up when lookup or capture fails.
  • Expect page-specific behavior: selectors, rendering timing, nested component structure, and browser/driver behavior vary by page. The examples use placeholders and do not claim a tested result on a particular site.
  • Account for infrastructure: Selenium requires a browser and driver environment that can reach the page. For remote execution, plan how screenshot bytes move to the system that needs them.
  • API pricing: Selenium itself is browser automation, so operational cost depends on the browser infrastructure you use. ScreenshotNeo’s stated plans range from free for 1,000 shots/month to paid tiers of $5 for 3,000, $15 for 15,000, $39 for 60,000, $99 for 250,000, and $249 for 1,000,000; yearly billing gives two months free.

9. FAQ

Can I use a normal CSS selector from the document to find a shadow child?

Locate the host in the document, then search for the child from the host’s shadow root. Each nested shadow boundary requires its own root lookup.

Does an element screenshot include the entire shadow tree?

It captures the element’s visible bounding region. It is not a guarantee that overflowing, clipped, or off-screen content will all be included.

Can I take a full-page screenshot with this exact Selenium workflow?

The cited Selenium materials distinguish driver and element screenshots but do not establish a portable full-page method here. Verify the behavior for your browser and driver.

When should I use ScreenshotNeo instead of Selenium?

Use it when you need a screenshot of a page URL without setting up browser automation. Use Selenium when you need to interact with the page and select a specific element inside its shadow DOM.