</>

Technology

Playwright

Difficulty

Advanced

Interview Question

How do you handle Shadow DOM elements in Playwright?

Answer

Shadow DOM in Playwright

Playwright automatically pierces Shadow DOM — you can locate elements inside Shadow DOM without any special handling, unlike Selenium which requires JavaScript workarounds.

What is Shadow DOM?

Shadow DOM is a web standard that encapsulates component internals — elements inside a shadow root are isolated from the main document's CSS and JavaScript.

HTML
<!-- Host element -->
<my-custom-button>
  #shadow-root (open)
    <button class="internal-btn">Click Me</button>
</my-custom-button>

Playwright Automatically Pierces Shadow DOM

TypeScript
// This works — Playwright looks inside shadow roots automatically
await page.getByRole('button', { name: 'Click Me' }).click();
await page.getByText('Click Me').click();

// CSS selector with >>> (explicit shadow piercing — older syntax)
await page.locator('my-custom-button >>> .internal-btn').click();

Standard Locators Work Inside Shadow DOM

TypeScript
// Web Components in action
test('interact with shadow DOM elements', async ({ page }) => {
  await page.goto('/component-library');

  // Shadow DOM inside <custom-input>
  const customInput = page.locator('custom-input');
  await customInput.getByRole('textbox').fill('Hello Shadow DOM');

  // Shadow DOM inside <custom-dropdown>
  await page.locator('custom-dropdown').click();
  await page.getByRole('option', { name: 'Option 2' }).click();

  // Shadow DOM inside <my-button>
  await page.locator('my-button').getByText('Submit').click();
});

Nested Shadow DOM

TypeScript
// Playwright handles multiple levels of shadow nesting
const innerButton = page.locator('outer-component')
  .locator('inner-component')
  .getByRole('button', { name: 'Action' });

await innerButton.click();

When CSS Doesn't Penetrate

If Playwright's auto-piercing doesn't work (very rare), use >>>:

TypeScript
// CSS with shadow piercing combinator
await page.locator('custom-form >>> input[name="email"]').fill('test@example.com');

Real-World Example — Material Web Components

TypeScript
test('fill Material Web form', async ({ page }) => {
  await page.goto('/material-form');

  // mwc-textfield uses Shadow DOM internally
  const emailField = page.locator('mwc-textfield[label="Email"]');
  await emailField.locator('input').fill('user@test.com');

  // mwc-button
  await page.locator('mwc-button[label="Submit"]').click();
});

Testing Shadow DOM via JavaScript (Fallback)

TypeScript
// If Playwright's auto-piercing fails for any reason
await page.evaluate(() => {
  const host = document.querySelector('my-button');
  const shadow = host?.shadowRoot;
  const btn = shadow?.querySelector('button') as HTMLButtonElement;
  btn?.click();
});

Key Advantage Over Selenium: Selenium cannot locate elements inside closed/open shadow roots with standard findElement(). You had to use executeJavaScript(). Playwright handles it natively, making Web Component testing effortless.

Follow AutomateQA

Related Topics