Shadow DOM is a web standard that enables encapsulation of markup, styles, and behavior within custom elements. Unlike regular DOM nodes, elements inside a shadow root are not directly accessible via standard Selenium locators (e.g., find_element(By.XPATH, ...)) because they reside in an isolated subtree.
This isolation is intentional — it prevents external scripts and styles from accidentally interfering with the component’s internal structure, much like how a <iframe> creates a boundary, but without requiring a separate document context.
Why Standard Locators Fail
Selenium operates on the light DOM by default. When a page contains a custom element such as <wujie-app> with an attached shadow root, any child elements (e.g., <button class="el-button">) inside that shadow tree remain invisible to conventional find methods unless explicitly traversed.
Accessing Shadow DOM in Selenium
The most reliable approach is to use JavaScript execution to pierce the shadow boundary:
- Locate the host element (the custom element that attaches the shadow root).
- Access its
shadowRootproperty. - Query inside the shadow root using standard DOM methods like
querySelector.
Example JavaScript snippet:
document.querySelector('wujie-app').shadowRoot.querySelector('button.el-button')
In Python with Selenium WebDriver, execute it like this:
shadow_host = driver.find_element(By.TAG_NAME, "wujie-app")
button_inside_shadow = driver.execute_script(
"return arguments[0].shadowRoot.querySelector('button.el-button');",
shadow_host
)
This pattern avoids fragile string-based JS injection and safely passes the WebElement reference into the script context.
Nested Shadow Roots
Some frameworks (e.g., Angular, Stencil) may nest multiple shadow roots. To reach deeper levels, chain shadowRoot acesses:
document.querySelector('outer-component')
.shadowRoot
.querySelector('inner-component')
.shadowRoot
.querySelector('button')
In Python, this becomes:
outer = driver.find_element(By.TAG_NAME, "outer-component")
inner_host = driver.execute_script("return arguments[0].shadowRoot.querySelector('inner-component');", outer)
final_button = driver.execute_script("return arguments[0].shadowRoot.querySelector('button');", inner_host)
Alternative: DevTools Copy JS Path
During manual inspection in browser DevTools (F12), right-clicking a shadow-DOM element and selecting Copy → Copy JS Path yields a working path like:
document.querySelector("wujie-app").shadowRoot.querySelector("button.el-button")
This can be adapted for use in execute_script(), though dynamic host resolution (as shown earlier) is preferred for maintainable automation.