Skip to main content
SeleniumDecoded

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 root
SearchContext shadowRoot = hostElement.getShadowRoot();
// Now find elements inside the shadow DOM
WebElement shadowButton = shadowRoot.findElement(By.cssSelector("#shadow-button"));
shadowButton.click();
// Find multiple elements
List<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 root
shadow_root = host_element.shadow_root
# Now find elements inside the shadow DOM
shadow_button = shadow_root.find_element(By.CSS_SELECTOR, "#shadow-button")
shadow_button.click()
# Find multiple elements
shadow_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 root
const shadowRoot = await hostElement.getShadowRoot();
// Now find elements inside the shadow DOM
const shadowButton = await shadowRoot.findElement(By.css('#shadow-button'));
await shadowButton.click();
// Find multiple elements
const 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 root
ISearchContext shadowRoot = hostElement.GetShadowRoot();
// Now find elements inside the shadow DOM
IWebElement shadowButton = shadowRoot.FindElement(By.CssSelector("#shadow-button"));
shadowButton.Click();
// Find multiple elements
var 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 roots
WebElement 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 roots
outer_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 roots
const 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 roots
IWebElement 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 roots
button = 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 roots
const 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 roots
IWebElement 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 JavaScript
WebElement shadowButton = (WebElement) js.executeScript(
"return document.querySelector('custom-element')" +
".shadowRoot.querySelector('#shadow-button')"
);
shadowButton.click();
// For nested shadow DOM
WebElement deepButton = (WebElement) js.executeScript(
"return document.querySelector('outer-component')" +
".shadowRoot.querySelector('inner-component')" +
".shadowRoot.querySelector('#deep-button')"
);
# Access shadow root via JavaScript
shadow_button = driver.execute_script("""
return document.querySelector('custom-element')
.shadowRoot.querySelector('#shadow-button')
""")
shadow_button.click()
# For nested shadow DOM
deep_button = driver.execute_script("""
return document.querySelector('outer-component')
.shadowRoot.querySelector('inner-component')
.shadowRoot.querySelector('#deep-button')
""")
// Access shadow root via JavaScript
const shadowButton = await driver.executeScript(`
return document.querySelector('custom-element')
.shadowRoot.querySelector('#shadow-button')
`);
await shadowButton.click();
// For nested shadow DOM
const 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 JavaScript
IWebElement shadowButton = (IWebElement)js.ExecuteScript(
@"return document.querySelector('custom-element')
.shadowRoot.querySelector('#shadow-button')"
);
shadowButton.Click();
// For nested shadow DOM
IWebElement 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 settings
driver.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 settings
driver.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 settings
await 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 settings
driver.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

LimitationDescription
CSS selectors onlyShadow root findElement only supports CSS selectors, not XPath
Open shadow roots onlyClosed shadow roots (mode: 'closed') cannot be accessed
No cross-boundary XPathXPath cannot traverse into shadow DOM
PerformanceDeep 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;
}
// Usage
WebElement 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
# Usage
component = driver.find_element(By.CSS_SELECTOR, "my-component")
if has_shadow_root(driver, component):
shadow = component.shadow_root
# Work with shadow DOM
async function hasShadowRoot(driver, element) {
const shadowRoot = await driver.executeScript(
'return arguments[0].shadowRoot', element
);
return shadowRoot !== null;
}
// Usage
const 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;
}
// Usage
IWebElement component = driver.FindElement(By.CssSelector("my-component"));
if (HasShadowRoot(driver, component))
{
ISearchContext shadow = component.GetShadowRoot();
// Work with shadow DOM
}

Best Practices

  1. Use Selenium 4’s native support when possible - it’s cleaner and more reliable
  2. Create helper methods for commonly accessed shadow DOM structures
  3. Document the shadow DOM structure for your application
  4. Use CSS selectors - XPath won’t work inside shadow roots
  5. Handle missing shadow roots gracefully - components may not always render with shadow DOM

Next Steps