## 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

```text
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

1. Confirm the shadow root is open: in devtools, the element shows `#shadow-root (open)`. Expected: open means pierceable.
2. Use a normal Playwright locator: `page.locator('my-card').getByRole('button', { name: 'OK' })`. Expected: matches across the shadow boundary.
3. 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.
4. For CSS piercing explicitly, Playwright supports `css=light-dom >> css=shadow-content` chains. Expected: manual control when auto-piercing is ambiguous.
5. 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).
- `querySelector` in 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.
- `getByTestId` inside 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
