Pierce the shadow DOM
Standard locators reach into open shadow roots, so web components need no special selector.
Web components hide their internals in a shadow root. Playwright's selector engine resolves through open shadow roots by default, so getByRole(), getByText(), and CSS locators reach the elements inside a component without any special syntax. You write the same locators you would for regular DOM.
Target an element inside a component
Point a normal locator at the control you want. It resolves whether the control sits in the light DOM or inside an open shadow root.
$context = Playwright::chromium();
$page = $context->newPage();
$page->goto('https://app.example.test/settings');
$page->getByRole('button', ['name' => 'Save profile'])->click();
expect($page->getByText('Profile saved'))->toBeVisible();
Combine light DOM and shadow DOM in one chain
A CSS locator chain crosses the shadow boundary between steps. This selects an element inside a component that lives inside a host element, without naming the shadow root.
$page->locator('user-card')->getByRole('link', ['name' => 'Edit'])->click();
The user-card custom element exposes an open shadow root, so the getByRole() step resolves inside it.
Limits to know
Two cases do not resolve, and they fail the same way a missing element does: the action waits, then times out.
- Closed shadow roots. A component created with
attachShadow({ mode: 'closed' })hides its tree from every selector engine. Nothing outside the component can reach in. Ask the component to expose the state you need, or assert on what it renders into the light DOM. - CSS shadow-piercing combinators. Deep combinators such as
>>>are not part of the engine. Rely on role, text, and test-id locators, which cross open boundaries on their own.
Expected result
For open shadow roots, the normal locator reaches the control and the page shows the expected result. For closed roots, the locator times out; assert the component's public output instead.