Hard Mode Shadow DOM Maze
Hard Mode
Hard Mode · Web components

Shadow DOM Maze

Playwright's locators pierce open shadow roots as if they were not there, right up until they cannot. Four levels: a plain open root, shadow nested inside shadow with slotted content, a closed root (the wall), and the boss room, an iframe living inside a shadow root. First time with shadow DOM? Start on the basic shadow DOM widget.

4Levels
3Custom elements
1Closed root
1Iframe boss

L1 Open shadow root

<tta-subscribe> renders its form inside an open shadow root. Normal locators walk straight in.

Goal: fill the email inside the component and click Subscribe with plain getByRole / getByPlaceholder, then assert the confirmation line. No special shadow API needed.

L2 Shadow inside shadow, plus slots

<tta-panel> (shadow) wraps a nested <tta-subscribe> (its own shadow) and a slotted light-DOM note.

This note lives in the light DOM and is projected through a slot.
Goal: subscribe inside the NESTED component, and separately assert the slotted note text. Notice both resolve with ordinary locators; nesting depth is irrelevant for open roots.

L3 The closed root

<tta-vault> attaches its shadow with mode: "closed". Its internals are invisible to every locator engine.

Goal: prove the wall exists: a locator for the button INSIDE the vault times out. Then use the escape hatches real apps provide: the host's data-count attribute mirrors the hidden counter, and the visible summary line below the component updates. Assert those instead.

Vault clicks so far: 0

L4 Boss: iframe inside a shadow root

<tta-embed> hosts an iframe (srcdoc) inside its open shadow root. Two boundaries stacked.

Goal: reach the button inside the frame inside the shadow. Use page.frameLocator('iframe#inner-embed') (the shadow boundary is pierced to find the iframe, the frame boundary needs frameLocator), click Approve, and assert the frame's status text.

Drills

  1. Level 1: subscribe with page.getByPlaceholder('[email protected]') and assert "Subscribed" appears. Zero shadow-specific code.
  2. Level 2: scope with page.getByTestId('level2-widget') and subscribe inside it; assert the slotted note with getByTestId('level2-slotted-note').
  3. Level 3: write the failing locator first, catch the timeout, then assert data-count flips to 3 after three clicks using expect(locator).toHaveAttribute(). Click through the vault's visible host area.
  4. Level 4: chain frameLocator through the shadow-hosted iframe and complete the approval, asserting on both sides of the frame boundary.
  5. Bonus: run locator.count() for button across the whole page and explain which buttons the number misses, and why.