Shadow DOM
Access and interact with elements inside Shadow DOM using Selenium's shadow root methods.
Selenium 4 Stable
Shadow DOM provides encapsulation for web components, creating a separate DOM tree that’s isolated from the main document. Selenium 4 introduced native support for accessing shadow roots.
What is Shadow DOM?
<!-- Regular DOM --><custom-element> #shadow-root (open) <div class="shadow-content"> <button id="shadow-button">Click Me</button> </div></custom-element>Elements inside the shadow root are not directly accessible via normal selectors from the main document.
Selenium 4: Native Shadow Root Support
Access Shadow DOM Elements
Selenium 4 Stable
import org.openqa.selenium.SearchContext;import org.openqa.selenium.WebElement;
// Find the host element (the custom element)WebElement hostElement = driver.findElement(By.cssSelector("custom-element"));
// Get the shadow rootSearchContext shadowRoot = hostElement.getShadowRoot();
// Now find elements inside the shadow DOMWebElement shadowButton = shadowRoot.findElement(By.cssSelector("#shadow-button"));shadowButton.click();
// Find multiple elementsList<WebElement> shadowItems = shadowRoot.findElements(By.cssSelector(".item"));from selenium.webdriver.common.by import By
# Find the host element (the custom element)host_element = driver.find_element(By.CSS_SELECTOR, "custom-element")
# Get the shadow rootshadow_root = host_element.shadow_root
# Now find elements inside the shadow DOMshadow_button = shadow_root.find_element(By.CSS_SELECTOR, "#shadow-button")shadow_button.click()
# Find multiple elementsshadow_items = shadow_root.find_elements(By.CSS_SELECTOR, ".item")const { By } = require('selenium-webdriver');
// Find the host element (the custom element)const hostElement = await driver.findElement(By.css('custom-element'));
// Get the shadow rootconst shadowRoot = await hostElement.getShadowRoot();
// Now find elements inside the shadow DOMconst shadowButton = await shadowRoot.findElement(By.css('#shadow-button'));await shadowButton.click();
// Find multiple elementsconst shadowItems = await shadowRoot.findElements(By.css('.item'));using OpenQA.Selenium;
// Find the host element (the custom element)IWebElement hostElement = driver.FindElement(By.CssSelector("custom-element"));
// Get the shadow rootISearchContext shadowRoot = hostElement.GetShadowRoot();
// Now find elements inside the shadow DOMIWebElement shadowButton = shadowRoot.FindElement(By.CssSelector("#shadow-button"));shadowButton.Click();
// Find multiple elementsvar shadowItems = shadowRoot.FindElements(By.CssSelector(".item"));Nested Shadow DOM
Web components can contain other web components with their own shadow roots:
Navigate Nested Shadow DOM
Selenium 4 Stable
// Structure:// <outer-component>// #shadow-root// <inner-component>// #shadow-root// <button id="deep-button">
// Navigate through nested shadow rootsWebElement outerHost = driver.findElement(By.cssSelector("outer-component"));SearchContext outerShadow = outerHost.getShadowRoot();
WebElement innerHost = outerShadow.findElement(By.cssSelector("inner-component"));SearchContext innerShadow = innerHost.getShadowRoot();
WebElement deepButton = innerShadow.findElement(By.cssSelector("#deep-button"));deepButton.click();# Structure:# <outer-component># #shadow-root# <inner-component># #shadow-root# <button id="deep-button">
# Navigate through nested shadow rootsouter_host = driver.find_element(By.CSS_SELECTOR, "outer-component")outer_shadow = outer_host.shadow_root
inner_host = outer_shadow.find_element(By.CSS_SELECTOR, "inner-component")inner_shadow = inner_host.shadow_root
deep_button = inner_shadow.find_element(By.CSS_SELECTOR, "#deep-button")deep_button.click()// Structure:// <outer-component>// #shadow-root// <inner-component>// #shadow-root// <button id="deep-button">
// Navigate through nested shadow rootsconst outerHost = await driver.findElement(By.css('outer-component'));const outerShadow = await outerHost.getShadowRoot();
const innerHost = await outerShadow.findElement(By.css('inner-component'));const innerShadow = await innerHost.getShadowRoot();
const deepButton = await innerShadow.findElement(By.css('#deep-button'));await deepButton.click();// Structure:// <outer-component>// #shadow-root// <inner-component>// #shadow-root// <button id="deep-button">
// Navigate through nested shadow rootsIWebElement outerHost = driver.FindElement(By.CssSelector("outer-component"));ISearchContext outerShadow = outerHost.GetShadowRoot();
IWebElement innerHost = outerShadow.FindElement(By.CssSelector("inner-component"));ISearchContext innerShadow = innerHost.GetShadowRoot();
IWebElement deepButton = innerShadow.FindElement(By.CssSelector("#deep-button"));deepButton.Click();Helper Method for Deep Shadow Access
Create a utility to simplify navigating shadow DOM:
Shadow DOM Helper
Selenium 4 Stable
public class ShadowDomHelper {
public static WebElement findInShadow(WebDriver driver, String... selectors) { SearchContext context = driver;
for (int i = 0; i < selectors.length; i++) { WebElement element = context.findElement(By.cssSelector(selectors[i]));
// If not the last selector, get shadow root if (i < selectors.length - 1) { context = element.getShadowRoot(); } else { return element; } } return null; }}
// Usage: Find button through multiple shadow roots// Selectors alternate: host > shadow content > host > shadow content...WebElement button = ShadowDomHelper.findInShadow(driver, "outer-component", // Host 1 "inner-component", // Shadow content of host 1, also host 2 "#deep-button" // Shadow content of host 2);def find_in_shadow(driver, *selectors): """Navigate through shadow roots to find an element.""" context = driver
for i, selector in enumerate(selectors): element = context.find_element(By.CSS_SELECTOR, selector)
# If not the last selector, get shadow root if i < len(selectors) - 1: context = element.shadow_root else: return element
return None
# Usage: Find button through multiple shadow rootsbutton = find_in_shadow(driver, "outer-component", # Host 1 "inner-component", # Shadow content of host 1, also host 2 "#deep-button" # Shadow content of host 2)async function findInShadow(driver, ...selectors) { let context = driver;
for (let i = 0; i < selectors.length; i++) { const element = await context.findElement(By.css(selectors[i]));
// If not the last selector, get shadow root if (i < selectors.length - 1) { context = await element.getShadowRoot(); } else { return element; } } return null;}
// Usage: Find button through multiple shadow rootsconst button = await findInShadow(driver, 'outer-component', // Host 1 'inner-component', // Shadow content of host 1, also host 2 '#deep-button' // Shadow content of host 2);public static class ShadowDomHelper{ public static IWebElement FindInShadow(IWebDriver driver, params string[] selectors) { ISearchContext context = driver;
for (int i = 0; i < selectors.Length; i++) { IWebElement element = context.FindElement(By.CssSelector(selectors[i]));
// If not the last selector, get shadow root if (i < selectors.Length - 1) { context = element.GetShadowRoot(); } else { return element; } } return null; }}
// Usage: Find button through multiple shadow rootsIWebElement button = ShadowDomHelper.FindInShadow(driver, "outer-component", // Host 1 "inner-component", // Shadow content of host 1, also host 2 "#deep-button" // Shadow content of host 2);JavaScript Fallback (Selenium 3)
For Selenium 3 or older browsers, use JavaScript to access shadow roots:
JavaScript Shadow DOM Access
Selenium 3 & 4 Stable
import org.openqa.selenium.JavascriptExecutor;
JavascriptExecutor js = (JavascriptExecutor) driver;
// Access shadow root via JavaScriptWebElement shadowButton = (WebElement) js.executeScript( "return document.querySelector('custom-element')" + ".shadowRoot.querySelector('#shadow-button')");shadowButton.click();
// For nested shadow DOMWebElement deepButton = (WebElement) js.executeScript( "return document.querySelector('outer-component')" + ".shadowRoot.querySelector('inner-component')" + ".shadowRoot.querySelector('#deep-button')");# Access shadow root via JavaScriptshadow_button = driver.execute_script(""" return document.querySelector('custom-element') .shadowRoot.querySelector('#shadow-button')""")shadow_button.click()
# For nested shadow DOMdeep_button = driver.execute_script(""" return document.querySelector('outer-component') .shadowRoot.querySelector('inner-component') .shadowRoot.querySelector('#deep-button')""")// Access shadow root via JavaScriptconst shadowButton = await driver.executeScript(` return document.querySelector('custom-element') .shadowRoot.querySelector('#shadow-button')`);await shadowButton.click();
// For nested shadow DOMconst deepButton = await driver.executeScript(` return document.querySelector('outer-component') .shadowRoot.querySelector('inner-component') .shadowRoot.querySelector('#deep-button')`);IJavaScriptExecutor js = (IJavaScriptExecutor)driver;
// Access shadow root via JavaScriptIWebElement shadowButton = (IWebElement)js.ExecuteScript( @"return document.querySelector('custom-element') .shadowRoot.querySelector('#shadow-button')");shadowButton.Click();
// For nested shadow DOMIWebElement deepButton = (IWebElement)js.ExecuteScript( @"return document.querySelector('outer-component') .shadowRoot.querySelector('inner-component') .shadowRoot.querySelector('#deep-button')");Real-World Example: Chrome Settings
Chrome’s settings page uses extensive Shadow DOM:
Chrome Settings Shadow DOM
Selenium 4 Medium
// Navigate to Chrome settingsdriver.get("chrome://settings/passwords");
// The settings page has deep shadow DOM nesting// settings-ui > settings-main > settings-basic-page > ...
WebElement settingsUi = driver.findElement(By.cssSelector("settings-ui"));SearchContext shadow1 = settingsUi.getShadowRoot();
WebElement settingsMain = shadow1.findElement(By.cssSelector("settings-main"));SearchContext shadow2 = settingsMain.getShadowRoot();
// Continue navigating through shadow roots...# Navigate to Chrome settingsdriver.get("chrome://settings/passwords")
# The settings page has deep shadow DOM nesting# settings-ui > settings-main > settings-basic-page > ...
settings_ui = driver.find_element(By.CSS_SELECTOR, "settings-ui")shadow1 = settings_ui.shadow_root
settings_main = shadow1.find_element(By.CSS_SELECTOR, "settings-main")shadow2 = settings_main.shadow_root
# Continue navigating through shadow roots...// Navigate to Chrome settingsawait driver.get('chrome://settings/passwords');
// The settings page has deep shadow DOM nesting// settings-ui > settings-main > settings-basic-page > ...
const settingsUi = await driver.findElement(By.css('settings-ui'));const shadow1 = await settingsUi.getShadowRoot();
const settingsMain = await shadow1.findElement(By.css('settings-main'));const shadow2 = await settingsMain.getShadowRoot();
// Continue navigating through shadow roots...// Navigate to Chrome settingsdriver.Navigate().GoToUrl("chrome://settings/passwords");
// The settings page has deep shadow DOM nesting// settings-ui > settings-main > settings-basic-page > ...
IWebElement settingsUi = driver.FindElement(By.CssSelector("settings-ui"));ISearchContext shadow1 = settingsUi.GetShadowRoot();
IWebElement settingsMain = shadow1.FindElement(By.CssSelector("settings-main"));ISearchContext shadow2 = settingsMain.GetShadowRoot();
// Continue navigating through shadow roots...Limitations
| Limitation | Description |
|---|---|
| CSS selectors only | Shadow root findElement only supports CSS selectors, not XPath |
| Open shadow roots only | Closed shadow roots (mode: 'closed') cannot be accessed |
| No cross-boundary XPath | XPath cannot traverse into shadow DOM |
| Performance | Deep nesting requires multiple API calls |
Detecting Shadow DOM
Check if an element has a shadow root:
Detect Shadow Root
Selenium 4 Stable
public static boolean hasShadowRoot(WebDriver driver, WebElement element) { JavascriptExecutor js = (JavascriptExecutor) driver; Object shadowRoot = js.executeScript("return arguments[0].shadowRoot", element); return shadowRoot != null;}
// UsageWebElement component = driver.findElement(By.cssSelector("my-component"));if (hasShadowRoot(driver, component)) { SearchContext shadow = component.getShadowRoot(); // Work with shadow DOM}def has_shadow_root(driver, element): shadow_root = driver.execute_script("return arguments[0].shadowRoot", element) return shadow_root is not None
# Usagecomponent = driver.find_element(By.CSS_SELECTOR, "my-component")if has_shadow_root(driver, component): shadow = component.shadow_root # Work with shadow DOMasync function hasShadowRoot(driver, element) { const shadowRoot = await driver.executeScript( 'return arguments[0].shadowRoot', element ); return shadowRoot !== null;}
// Usageconst component = await driver.findElement(By.css('my-component'));if (await hasShadowRoot(driver, component)) { const shadow = await component.getShadowRoot(); // Work with shadow DOM}public static bool HasShadowRoot(IWebDriver driver, IWebElement element){ IJavaScriptExecutor js = (IJavaScriptExecutor)driver; object shadowRoot = js.ExecuteScript("return arguments[0].shadowRoot", element); return shadowRoot != null;}
// UsageIWebElement component = driver.FindElement(By.CssSelector("my-component"));if (HasShadowRoot(driver, component)){ ISearchContext shadow = component.GetShadowRoot(); // Work with shadow DOM}Best Practices
- Use Selenium 4’s native support when possible - it’s cleaner and more reliable
- Create helper methods for commonly accessed shadow DOM structures
- Document the shadow DOM structure for your application
- Use CSS selectors - XPath won’t work inside shadow roots
- Handle missing shadow roots gracefully - components may not always render with shadow DOM
Next Steps
- JavaScript Executor - Advanced DOM manipulation
- Iframes - Another DOM isolation pattern
- Explicit Waits - Wait for shadow DOM elements