How to Capture a Screenshot of a Shadow DOM Element with Selenium
Find the element through its shadow root, then use Selenium’s element screenshot API to save a PNG or keep the image bytes in memory.
To capture a screenshot of an element inside a shadow DOM with Selenium, locate its host, access host.shadow_root, find the target from that root, and call the target WebElement’s screenshot method. Selenium 4 or later provides the documented shadow-root methods. The element screenshot API returns PNG data or saves a PNG file.
1. Install Selenium and start a browser
Install Selenium in the Python environment used by your script:
python -m pip install selenium
The following complete example starts Chrome, opens a page, finds an element inside a shadow root, saves its screenshot, and closes the browser. Replace the example URL and selectors with the page’s URL, shadow host, and target selectors.
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By
url = "https://example.com"
host_selector = "my-component"
target_selector = ".target"
output_path = Path("shadow-element.png").resolve()
options = webdriver.ChromeOptions()
# Uncomment to run without a visible browser window:
# options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
try:
driver.get(url)
host = driver.find_element(By.CSS_SELECTOR, host_selector)
shadow_root = host.shadow_root
target = shadow_root.find_element(By.CSS_SELECTOR, target_selector)
target.screenshot(str(output_path))
print(f"Saved screenshot to {output_path}")
finally:
driver.quit()
Selenium’s finder guide documents finding the host from the driver and then finding descendants from the shadow root. Use a full output path when saving screenshots, as recommended by the Python API. Selenium: Finding web elements · Selenium Python WebElement API.
2. Find the target through the shadow root
A shadow tree is scoped beneath a host element. A top-level driver.find_element search does not directly search inside that tree. Get the host first, obtain its shadow root, and search from the root:
host = driver.find_element(By.CSS_SELECTOR, "my-component")
root = host.shadow_root
target = root.find_element(By.CSS_SELECTOR, ".target")
The selectors are examples: inspect the page to identify the actual custom-element host and a selector for the desired descendant. The target returned by the root-scoped search is a Selenium WebElement, so you can use its element screenshot API.
Nested shadow roots
For nested components, repeat the host-to-root lookup at each shadow boundary. Search for the inner host within the outer root, then obtain that host’s root and search there:
outer_host = driver.find_element(By.CSS_SELECTOR, "outer-component")
outer_root = outer_host.shadow_root
inner_host = outer_root.find_element(By.CSS_SELECTOR, "inner-component")
inner_root = inner_host.shadow_root
target = inner_root.find_element(By.CSS_SELECTOR, ".target")
target.screenshot("nested-shadow-element.png")
Each lookup begins from the context that contains the next host or target. If a component uses slots, inspect which element is actually rendered and where it belongs in the DOM; a slotted node may be located differently from a descendant authored inside the component’s shadow tree.
3. Save a PNG file or keep PNG bytes
Use screenshot(path) to write the screenshot to a file. Use screenshot_as_png when another step should consume the image in memory:
png_bytes = target.screenshot_as_png
with open("shadow-element.png", "wb") as image_file:
image_file.write(png_bytes)
Both documented choices produce PNG output. The property returns binary PNG screenshot data, while screenshot(path) writes a PNG file. See the Python WebElement API.
4. Wait for the host and target when the page is dynamic
Custom elements may attach their shadow roots or render their descendants after the initial document load. A direct lookup can run too early. Wait for the host, then poll for its shadow root and target. This example retries the lookup until the target is available or the wait expires:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchShadowRootException, NoSuchElementException
host = WebDriverWait(driver, 15).until(
lambda d: d.find_element(By.CSS_SELECTOR, "my-component")
)
def find_shadow_target(_driver):
try:
return host.shadow_root.find_element(By.CSS_SELECTOR, ".target")
except (NoSuchShadowRootException, NoSuchElementException):
return False
target = WebDriverWait(driver, 15).until(find_shadow_target)
target.screenshot("shadow-element.png")
Choose a wait timeout suited to the application. If the element is inside nested roots, poll the complete sequence of root-scoped lookups, returning the final target only after every boundary and descendant is available.
5. Use other Selenium languages
The title asks about Selenium generally; the full runnable example above uses Python. These compact patterns show the same lookup and capture operation in Java and JavaScript. They assume Selenium 4 and an already-started driver with the page loaded.
Java
import java.io.File;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
ChromeDriver driver = new ChromeDriver();
try {
driver.get("https://example.com");
WebElement host = driver.findElement(By.cssSelector("my-component"));
WebElement target = host.getShadowRoot().findElement(By.cssSelector(".target"));
File png = target.getScreenshotAs(OutputType.FILE);
System.out.println("Saved screenshot: " + png.getAbsolutePath());
} finally {
driver.quit();
}
JavaScript
const { Builder, By } = require('selenium-webdriver');
const fs = require('node:fs/promises');
(async () => {
const driver = await new Builder().forBrowser('chrome').build();
try {
await driver.get('https://example.com');
const host = await driver.findElement(By.css('my-component'));
const root = await host.getShadowRoot();
const target = await root.findElement(By.css('.target'));
const pngBase64 = await target.takeScreenshot();
await fs.writeFile('shadow-element.png', Buffer.from(pngBase64, 'base64'));
} finally {
await driver.quit();
}
})();
In either language, nested roots require repeating the same host, root, and descendant sequence. The language bindings and browser-driver versions must support the shadow-root operations used by your setup.
6. Understand element screenshot behavior and limits
- Capture a descendant WebElement. Locate the exact element inside the root and invoke its screenshot method. Selenium documents element screenshots; use the descendant rather than treating a shadow root itself as the screenshot target.
- Expect PNG output. The documented Python methods return PNG bytes or save a PNG file.
- Check visibility and layout. If the target is hidden, has no rendered size, or is outside the usable page state, the capture may fail or not show the content you expect. Make the application render the target before capturing.
- Account for nested roots. A selector cannot cross multiple shadow boundaries in one ordinary root-scoped lookup. Resolve each host and root in sequence.
- Keep browser compatibility specific. Selenium’s Python reference lists Chromium, Firefox, and Safari for
shadow_root. The finder guide requires Selenium 4 or greater for the documented shadow-root methods. Confirm the exact Selenium, browser, and driver versions deployed; those references do not establish a version-by-version guarantee for every end-to-end screenshot combination.
The element screenshot operation maps to WebDriver’s element screenshot endpoint. Selenium Python remote connection API.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
NoSuchShadowRoot / NoSuchShadowRootException |
The selected host has no attached shadow root yet, or it is not the component host. | Verify the host selector and wait until the component attaches its root. Selenium documents this error when an element has no attached shadow root. |
| Target lookup returns no element | The selector is being searched from the wrong context, the selector is wrong, or the target has not rendered. | Search from the correct shadow root, verify the descendant selector, and wait for dynamic rendering. For nested components, resolve each root boundary separately. |
| The screenshot contains the wrong content or is empty | The target is not the intended descendant, has not rendered, is hidden, or has no useful rendered size. | Confirm the located element, wait for content to render, and check the target’s visibility and layout before capture. |
| Shadow-root methods are unavailable | The installed Selenium binding is older than the documented Selenium 4 requirement, or the language/browser setup differs. | Use Selenium 4 or later and verify the binding, browser, and driver versions together. |
| The file cannot be found after capture | The script wrote to a relative path in a different working directory than expected. | Resolve and print an absolute path, or write the returned PNG bytes to an explicit destination. |
| Capture fails after a long page load | The page or target did not reach the state expected by the script before timeout. | Wait for the host and target explicitly; investigate page loading separately from shadow-root lookup. |
For root-scoped lookup behavior and the shadow-root error, see the Selenium Python ShadowRoot API and WebElement API.
8. Performance, reliability, and cost
An element screenshot is useful when you need just one component image instead of a full-page capture. Reuse the browser session when capturing multiple elements on the same page, and wait only for the component state you need. Avoid fixed delays when a condition-based wait can identify readiness. Selenium’s documented API pages do not provide comparative performance benchmarks for these choices.
For reliable automation, use explicit waits around dynamic components, close the driver in a finally block, and write output to a known path. Pin and verify compatible Selenium, browser, and driver versions in the environment that runs the capture. Selenium itself is software you run; infrastructure, browser execution, and maintenance costs depend on your environment, so the cited documentation does not establish a per-screenshot price.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request captures a URL as PNG, JPEG, WebP, or PDF. This captures a page URL rather than targeting an arbitrary shadow DOM descendant as Selenium does; use Selenium when you need that precise element-level operation. For ordinary page captures, ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed, and responses identify page verdict and billing status. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan. See the ScreenshotNeo API documentation.
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}`);
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Can I screenshot the shadow root itself?
The documented element screenshot API is called on a located WebElement. Find the descendant you want inside the root and capture that element.
Does this return a JPEG?
The documented Python element screenshot methods produce PNG output. Convert the PNG afterward if a different image format is required.
Can I use this with Firefox or Safari?
The Selenium Python reference lists Chromium, Firefox, and Safari for the shadow_root property. Verify the exact installed browser, driver, and Selenium versions for your deployment.
How do I find an element inside a shadow DOM with Selenium?
Locate the host from the driver, access its shadow_root, then search for the descendant from that root. Repeat this sequence for each nested shadow root.


