Skip to contentPlayForge

Should I use CSS or XPath selectors in Playwright?

Written by Jatin Thacker, Automation Architect · Updated

Prefer neither: Playwright recommends role, text, label and test id locators first, because CSS and XPath tend to break when the markup changes. When you do need a raw selector, choose a short CSS selector on a stable attribute, because CSS reaches into open shadow roots and XPath does not.

This answer is one piece of Chapter 10: Locators II: Scoping and Rot.

  • Scoping and Filtering
  • CSS Selectors, and the Ones That Rot
  • XPath, and What Replaced It
  • Locators That Rot
  • Locator Factories
  • Practice and interview questions: Locators Masterclass

Playwright accepts both through page.locator(), and it detects which one you mean: a string starting with // or .. is treated as XPath, anything else as CSS by default. Its documentation gives both the same warning: they are tied to the implementation and can break when the page changes.

They are not equal, though. Playwright's CSS engine pierces open shadow DOM. XPath does not, so anything inside a web component's shadow root is invisible to it.

The example shows this on the embeds page, where the supplier rating input lives inside an open shadow root. The CSS selector [data-testid="shadow-rating-input"] finds one element. The XPath expression that asks for the same attribute finds none. getByTestId() finds the input without either.

So the order is: a built-in locator first. If the markup gives you nothing user-facing, a CSS selector on an attribute the team has agreed to keep, such as [data-sku="MRD-1003"]. XPath only when you truly need to walk the tree, and never across a shadow boundary.

qa-042.spec.js(https://practice.learnplayforge.com)
import { test, expect } from '@playwright/test'; test('CSS reaches into an open shadow root; XPath does not', async ({ page }) => { await page.goto('/embeds.html?mock=static&seed=42'); // The rating input lives inside an open shadow root await expect(page.locator('[data-testid="shadow-rating-input"]')).toHaveCount(1); await expect(page.locator('//*[@data-testid="shadow-rating-input"]')).toHaveCount(0); // The user-facing locator needs neither await expect(page.getByTestId('shadow-rating-input')).toBeVisible(); });
Output
1 passed (1.9s)
Test yourself
An input sits inside an open shadow root. Which of these locators will find nothing?

Practised in the full course: the drill Open roots need no API.

See the chapter

AI can write the test now. The job is knowing whether it checks anything. That's what the course teaches, one chapter at a time.

163 lessons · 226 drills · start free

← All questions

Training a team? Talk to us.