Skip to main content

Lumos ShadowDOM 🪄

PyPI version Python versions License: MIT Tests

Automate Shadow DOM elements in Selenium with zero headache.

Getting NoSuchElementException for an element you can clearly see in DevTools? It is probably inside a shadow root. Shadow DOMs are tricky. They hide elements from standard Selenium commands, so driver.find_element() raises NoSuchElementException for elements inside a shadow root (web components, Lit, Salesforce LWC, design systems). lumos-shadowdom solves this by letting you traverse nested open shadow roots automatically using one simple path or even just visible text.

Learn more: Conquering Shadow DOM with Lumos · Project page


🚀 Quick Start

1. Install

pip install lumos-shadowdom

2. Use it in your code

Lumos adds new superpowers to your Selenium driver automatically!

from selenium import webdriver
import lumos  # <--- Import this to activate magic!

driver = webdriver.Chrome()
driver.get("https://example.com")

# 🟢 Scenario 1: You know the path (or found it using our tool below)
# Syntax: "host-element > nested-host > target-element"
driver.find_shadow("my-app > settings-panel > button#save").click()

# 🟢 Scenario 2: Find ALL matching elements in shadow DOM
buttons = driver.find_all_shadow("nav-menu > button.action")
for btn in buttons:
    print(btn.text)

# 🟢 Scenario 3: You just want to find a button with specific text
# This scans the ENTIRE page (including all shadow roots) for you.
driver.find_shadow_text("Confirm Purchase").click()

🕵️ How to Find the Path? (The Magic Tool)

Don't waste time inspecting elements manually. Use our Discovery Tool to generate the exact path instantly.

  1. Open your website in Chrome.
  2. Open DevTools (Press F12 or Right Click -> Inspect).
  3. Go to the Console tab.
  4. Paste the script below and hit Enter.
  5. Hold Alt and Click on any element in the page.
  6. The Console will show you the Short Path and Long Path. Copy the "Short Path" and use it in driver.find_shadow().

Copy this Script:

// Lumos Discovery Tool v2.0
// Paste this into Chrome Console, then Alt + Click any element.
document.addEventListener('click', function(e) {
    if (!e.altKey) return; 
    e.preventDefault();
    e.stopImmediatePropagation();

    // 1. Get the real target (piercing Shadow DOM)
    let composed = e.composedPath();
    let target = composed[0];
    
    let fullPath = [];
    let shortPath = [];
    let currentScope = [];

    // 2. Traverse up the tree
    for (let i = 0; i < composed.length; i++) {
        let node = composed[i];

        // Stop at document root
        if (node.nodeType !== Node.ELEMENT_NODE && !(node instanceof ShadowRoot)) {
             if (currentScope.length > 0) {
                 fullPath.unshift(currentScope.join(' '));
                 shortPath.unshift(getShortSelector(currentScope));
             }
             break;
        }

        // Handle Shadow Root Boundary
        if (node instanceof ShadowRoot) {
            fullPath.unshift(currentScope.join(' '));
            shortPath.unshift(getShortSelector(currentScope));
            currentScope = [];
            continue;
        }

        // Build Selector for current node
        let selector = node.tagName.toLowerCase();
        if (node.id) {
            selector += '#' + node.id;
        } else if (node.className && typeof node.className === 'string') {
            selector += '.' + node.className.trim().split(/\s+/).join('.');
        }
        currentScope.unshift(selector);
    }
    
    function getShortSelector(scope) {
        let short = [];
        for (let j = scope.length - 1; j >= 0; j--) {
            short.unshift(scope[j]);
            if (scope[j].includes('#')) break; // Stop at ID for brevity
        }
        return short.join(' ');
    }
    
    console.log(`%c 🪄 Lumos Path Discovered!`, 'background: #222; color: #bada55; font-size: 14px; padding: 4px;');
    console.log(`%c Short Path:`, 'font-weight: bold; color: #007bff;', shortPath.join(' > '));
    console.log(`%c Long Path: `, 'color: #666;', fullPath.join(' > '));
}, true);

🤔 Why not Selenium 4's shadow_root?

Selenium 4 can open a shadow root natively, but you need one hop per shadow host:

from selenium.webdriver.common.by import By

save = (
    driver.find_element(By.CSS_SELECTOR, "my-app").shadow_root
    .find_element(By.CSS_SELECTOR, "settings-panel").shadow_root
    .find_element(By.CSS_SELECTOR, "button#save")
)
Selenium 4 element.shadow_root driver.execute_script(...) by hand Lumos
Nested roots One find_element + .shadow_root per host Hand-written JS per page One path: "my-app > settings-panel > button#save"
Waiting None built in; wrap each hop in WebDriverWait yourself None built in Waits up to timeout (default 10 s) for the whole path
Find by visible text No Write it yourself driver.find_shadow_text("Confirm Purchase") searches every open shadow root
Selenium 3 Not available Yes Yes

What Lumos does not do: it can't reach closed shadow roots (mode='closed'), and it doesn't cross <iframe> boundaries, so switch with driver.switch_to.frame(...) first. See Limitations & Edge Cases below.


📚 API Reference

driver.find_shadow(path: str, timeout: int = 10) -> WebElement

Finds a single element inside nested shadow DOMs.

  • path: A string describing the path to the element. Use > to separate shadow boundaries.
    • Example: user-profile > #settings > input.name
    • Means: Find user-profile, enter its shadow root, find #settings, enter its shadow root, find input.name.
  • timeout: Seconds to wait for element (default: 10).
  • Returns: The matching WebElement.

driver.find_all_shadow(path: str, timeout: int = 10) -> List[WebElement]

Finds ALL elements matching the path inside nested shadow DOMs.

  • path: Same syntax as find_shadow. The last selector matches multiple elements.
    • Example: app-root > nav-menu > button returns all buttons in the nav.
  • Returns: List of matching WebElements (may be empty).

driver.find_shadow_text(text: str, timeout: int = 10) -> WebElement

Recursively searches every shadow root in the DOM for an element containing the specified text.

  • text: The text to look for (case-sensitive).
  • Returns: The first matching WebElement found.

❓ FAQ

Q: Why do I need this? A: Standard driver.find_element() cannot "see" inside a Shadow DOM. You usually have to write complex code to get the shadow root, then find the element, then repeat for nested roots. Lumos does this in one line.

Q: Does it work with Selenium 3? A: Yes! It is fully compatible with Selenium 3.141.0 and Selenium 4+.

Q: The Discovery Tool shows a path, but Lumos can't find it. A:

  1. Ensure the element is visible on the screen.
  2. If the path is very long, try using the "Short Path" provided by the tool.
  3. Check if the element is inside an iframe. Lumos currently supports Shadow DOM, but iframe support is separate (you must switch to the iframe first).

Q: Which exception do I get when nothing is found? A: find_shadow() and find_shadow_text() raise lumos.ElementNotFoundError (a LumosException) after the timeout, with the path or text in the message. find_all_shadow() returns an empty list after the timeout instead of raising.

Q: Can I use the CSS child combinator (>) inside one shadow root? A: In a string path, > always means "enter the next shadow root". Use a descendant selector (a space) inside a root, or use the Lumos class with a list of selectors, where each item can be any CSS selector: Lumos(driver).find_element(["my-app", "div.panel > settings-panel", "button#save"]).

Q: find_shadow_text() returns a different element than I expected. A: It returns the first element with no child elements whose innerText contains your text (case-sensitive), walking the page in document order and descending into each open shadow root as it meets it. Use a longer, more specific text, or use a path with find_shadow().


⚠️ Limitations & Edge Cases

1. Shadow DOM inside <iframe>

Lumos cannot automatically cross <iframe> boundaries because they are separate documents. Solution: Switch to the iframe first using Selenium, then use Lumos.

# 1. Switch to the iframe
iframe = driver.find_element(By.ID, "my-frame")
driver.switch_to.frame(iframe)

# 2. Now use Lumos inside the iframe
driver.find_shadow("host-inside-frame > button").click()

2. Closed Shadow Roots

Lumos cannot access Shadow DOMs created with mode='closed'. This is a browser security limitation that no tool can bypass standardly.


Author: Dhiraj Das · Follow @dkkkkd on X License: MIT (see LICENSE)

Metadata

Release files for lumos-shadowdom 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lumos-shadowdom 0.2.1
File Size Uploaded Provenance
lumos_shadowdom-0.2.1.tar.gz 14.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for lumos-shadowdom 0.2.1
File Interpreter ABI Platform
lumos_shadowdom-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 23.5 kB

Release files / lumos_shadowdom-0.2.1.tar.gz

Download URL lumos_shadowdom-0.2.1.tar.gz
Size 14.3 kB
Tags Source
SHA-256 checksum
How to use checksums
0780ffdfece15384fad81e26660d73cb4de8459327fe8e8e7bd390208638351e
BLAKE2b-256 checksum
How to use checksums
c76a18d644ab3b16ac2685338befd08a37b2ff696a24e666e895f7cba5fe9b9d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.7

Release files / lumos_shadowdom-0.2.1-py3-none-any.whl

Download URL lumos_shadowdom-0.2.1-py3-none-any.whl
Size 9.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cb6e7cea09f27b508b99bb4e42d86da32e3cc63408ab4ed75b7dac908a0fd557
BLAKE2b-256 checksum
How to use checksums
3bdecdd0fd894c878a1b5c3b188551e4bed00260b2118854f475911822859649
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.7

Security for lumos-shadowdom 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page