Playwright·Cookbook Field Manual
Part 09 · Interaction Toolkit Card 47

Frames & Shadow DOM

Card 47: Frames & Shadow DOM

The portable agent skills for this pattern are playwright-interactions and playwright-locators.

What This Pattern Solves

Payment widgets, embedded forms and chat bubbles often live in an <iframe>. Design-system components often render inside a shadow root. An iframe holds a second document, so you enter it with frameLocator(). A shadow root belongs to the same document, and Playwright’s locators walk into it when the root is open. Pick the right tool for each and the test needs no evaluate calls or deep CSS selectors.

How It Works

  1. Enter a frame: page.frameLocator('iframe[title="Payment"]'), or start from an accessible locator and call .contentFrame().
  2. Chain for nested frames: one .contentFrame() per document.
  3. Use normal locators inside: getByRole, getByLabel, getByText all work inside a frame locator.
  4. Assert across frames: act in the frame, assert the result wherever the app shows it, often the parent page.
  5. Shadow DOM: no special API. Role, text and CSS locators pierce open roots. XPath does not. Closed roots stay out of reach.

Code Example

// Bad: the button lives in the frame's document.
await expect(page.getByRole('button', { name: 'Subscribe' })).toHaveCount(0);

// Good: enter the frame first.
const frame = page.getByTitle('Newsletter').contentFrame();
await frame.getByLabel('Email').fill('ada@example.com');
await frame.getByRole('button', { name: 'Subscribe' }).click();
await expect(page.getByRole('status')).toHaveText('Subscribed');

// Nested frames: one step per document.
await page.getByTitle('Outer').contentFrame()
  .getByTitle('Inner').contentFrame()
  .getByRole('button', { name: 'Ping' }).click();

// Open shadow root: the same locator you would use without it.
await page.getByRole('button', { name: 'Shadow action' }).click();

Key Concepts

When to Use This Pattern

Isolated examples

The spec serves frame documents with page.route and builds shadow roots inline. Run the tests to see each boundary crossed.

Run This Example

pnpm test src/47-frames-and-shadow-dom