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.
<!-- Host element -->
<my-custom-button>
#shadow-root (open)
<button class="internal-btn">Click Me</button>
</my-custom-button>
Playwright Automatically Pierces Shadow DOM
// 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
// 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
// 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 >>>:
// CSS with shadow piercing combinator
await page.locator('custom-form >>> input[name="email"]').fill('test@example.com');
Real-World Example — Material Web Components
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)
// 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 useexecuteJavaScript(). Playwright handles it natively, making Web Component testing effortless.
