playwright shadow DOM: how to pierce shadow roots
Shows how Playwright locators pierce open shadow DOM. Use when the target is inside a shadow root. Not for closed shadow roots or iframes.
TL;DR
Playwright's locators pierce open shadow DOM automatically, unlike raw querySelector. Use normal locators (getByRole, CSS) and they cross shadow boundaries; only closed shadow roots need app cooperation.
Error
Error: locator.waitFor: Timeout 5000ms exceeded waiting for locator('my-button')
(The button is inside an open shadow root; raw querySelector from the document cannot see it.)Steps
- Confirm the shadow root is open: in devtools, the element shows
#shadow-root (open). Expected: open means pierceable. - Use a normal Playwright locator:
page.locator('my-card').getByRole('button', { name: 'OK' }). Expected: matches across the shadow boundary. - If it is closed (
#shadow-root (closed)), you cannot pierce it; ask the component team to expose the control or add a test hook. Expected: a testability fix in the app. - For CSS piercing explicitly, Playwright supports
css=light-dom >> css=shadow-contentchains. Expected: manual control when auto-piercing is ambiguous. - Re-run and confirm the locator resolves. Expected: green without special iframe-style helpers.
When to use
- Web components with shadow DOM (Lit, Stencil, native custom elements).
querySelectorin the console cannot find what Playwright should.
When not to use
- Closed shadow roots (not pierceable by design).
- Iframes (different mechanism).
Tool compatibility
- Playwright 1.30 through latest; piercing is built into the selector engine.
Variant phrasings
Playwright cannot find element in shadow DOM
Usually a closed root or a wrong outer scope; check open vs closed first.
Testing web components with Playwright
The broader topic; open shadow DOM is the easy case.
Why it happens
Shadow DOM encapsulates markup by design. Playwright's engine is shadow-aware, but only for open roots; closed roots are intentionally opaque.
Edge cases
- Some frameworks wrap components in multiple nested shadow roots; chain locators through each.
getByTestIdinside shadow DOM works if the attribute is on the inner element.- Closed roots in third-party widgets are a vendor testability issue; escalate, do not hack.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_qHjde-SRKScGjAAhmBXKvw
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.