Skip to main content
SeleniumDecoded

Find Elements in Nested Shadow DOM

Walk through several shadow roots to reach an element inside deeply nested web components.

Medium Verified against Selenium 4.48.0

Problem

A design system nests components three deep: <app-shell> contains <nav-bar> contains <search-box>, each with its own shadow root. By.cssSelector from the document cannot cross shadow boundaries, and XPath cannot see into them at all.

Solution

Chain getShadowRoot() calls, one per boundary, and search with CSS at each level. A helper that takes a list of selectors keeps tests readable.

Descend through shadow roots
Selenium 4 Medium
public static WebElement deep(WebDriver driver, String... cssPath) {
SearchContext ctx = driver;
WebElement el = null;
for (int i = 0; i < cssPath.length; i++) {
el = ctx.findElement(By.cssSelector(cssPath[i]));
if (i < cssPath.length - 1) ctx = el.getShadowRoot(); // step into the host's shadow tree
}
return el;
}
WebElement input = deep(driver, "app-shell", "nav-bar", "search-box", "input[type=search]");
input.sendKeys("selenium");
def deep(driver, *css_path):
ctx = driver
el = None
for i, selector in enumerate(css_path):
el = ctx.find_element(By.CSS_SELECTOR, selector)
if i < len(css_path) - 1:
ctx = el.shadow_root # step into the host's shadow tree
return el
search = deep(driver, "app-shell", "nav-bar", "search-box", "input[type=search]")
search.send_keys("selenium")
async function deep(driver, ...cssPath) {
let ctx = driver;
let el;
for (let i = 0; i < cssPath.length; i++) {
el = await ctx.findElement(By.css(cssPath[i]));
if (i < cssPath.length - 1) ctx = await el.getShadowRoot();
}
return el;
}
const input = await deep(driver, 'app-shell', 'nav-bar', 'search-box', 'input[type=search]');
await input.sendKeys('selenium');
public static IWebElement Deep(IWebDriver driver, params string[] cssPath)
{
ISearchContext ctx = driver;
IWebElement el = null!;
for (int i = 0; i < cssPath.Length; i++)
{
el = ctx.FindElement(By.CssSelector(cssPath[i]));
if (i < cssPath.Length - 1) ctx = el.GetShadowRoot();
}
return el;
}
var input = Deep(driver, "app-shell", "nav-bar", "search-box", "input[type=search]");
input.SendKeys("selenium");

Why It Works

Each shadow root is its own SearchContext. Selenium 4’s getShadowRoot() returns it, and CSS selectors run relative to it, so you descend one host at a time. JavaScript’s querySelector behaves identically, which is the fallback when a browser or old driver lacks shadow root support.

Gotchas

  • Only open shadow roots are reachable. Closed roots throw; ask the component authors to use open mode in test builds.
  • XPath never works inside shadow trees; use CSS.
  • Elements inside a shadow tree can still go stale when the component re-renders; combine with the stale-element retry recipe.
  • Firefox needs geckodriver 0.30+ and Selenium 4.0+ for getShadowRoot().

Learn the theory