CSS and XPath Selectors in Playwright: Complete Guide with Examples

Role-based locators are the first choice in Playwright, but every real project also needs CSS selectors and XPath. This lesson teaches both from scratch with cheat sheets and runnable examples on real practice pages.

Advertisement

๐Ÿ“– What You’ll Learn: how Playwright detects CSS and XPath, CSS selector syntax, XPath functions and axes, reading table cells three ways, chaining and filtering locators, strict mode, testing selectors in DevTools, and CSS vs XPath.


๐Ÿงญ Where CSS and XPath Fit in Playwright

Playwright recommends user-facing locators first, such as getByRole, getByLabel and getByText, because they match how users see the page and survive UI refactors. You learned them in Playwright Locators.

But real applications are messy: missing labels, duplicate text, generated tables, legacy markup. That is when CSS selectors and XPath become essential. Both work through page.locator() and get all Playwright benefits: auto-waiting, retries and strictness.

Recommended priority:

  1. getByRole (with accessible name)
  2. getByLabel, getByPlaceholder, getByText, getByAltText, getByTitle
  3. getByTestId (data-testid or your configured attribute)
  4. CSS selector (#id, [data-test=...], stable classes)
  5. XPath (complex relationships, text plus structure, legacy pages)

๐Ÿ”ค How Playwright Detects CSS vs XPath

page.locator('#user-name');                 // CSS (default)
page.locator('//input[@id="user-name"]');   // XPath: starts with //
page.locator('..');                         // XPath: parent of the current element

// Explicit prefixes (optional)
page.locator('css=#user-name');
page.locator('xpath=//input[@id="user-name"]');

Any selector starting with // or .. is treated as XPath; everything else is CSS unless you add a prefix.


๐ŸŽจ CSS Selectors Cheat Sheet

All examples use the real login page of https://www.saucedemo.com, whose inputs look like this:

<input placeholder="Username" data-test="username" id="user-name" name="user-name" type="text" class="input_error form_input">
<input placeholder="Password" data-test="password" id="password" name="password" type="password" class="input_error form_input">
<input data-test="login-button" id="login-button" type="submit" value="Login" class="submit-button btn_action">
SelectorMatchesExample
#idElement with that id#user-name
.classElement with that class.submit-button
tagAll elements of that taginput
tag.classTag with a classinput.form_input
[attr="value"]Exact attribute value[data-test="password"]
[attr^="v"]Attribute starts with[id^="user"]
[attr$="v"]Attribute ends with[id$="button"]
[attr*="v"]Attribute contains[placeholder*="ser"]
A BB anywhere inside Aform input
A > BB as a direct child of Aform > div
A + BB immediately after Alabel + input
:nth-child(n)The nth child of its parenttr:nth-child(2)
A, BEither A or B#user-name, #password
import { test, expect } from '@playwright/test';

test('login with CSS selectors', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.locator('#user-name').fill('standard_user');
  await page.locator('[data-test="password"]').fill('secret_sauce');
  await page.locator('input[type="submit"]').click();
  await expect(page).toHaveURL(/inventory/);
});

๐Ÿ’ก Tip: Prefer ids and test attributes (data-test, data-testid) over classes. Classes such as css-1x2y3z generated by CSS-in-JS libraries change on every build.


๐Ÿงฌ XPath Cheat Sheet

XPathMatches
//inputAny input anywhere
//input[@id='user-name']Input with id user-name
//input[@type='submit' and @value='Login']Both conditions
//input[@id='x' or @name='x']Either condition
//div[text()='Swag Labs']Element whose own text is exactly Swag Labs
//h4[contains(text(),'Accepted')]Text contains a substring
//input[starts-with(@id,'user')]Attribute starts with
//td[normalize-space()='Jason']Text with surrounding spaces trimmed
(//tr)[2]The second tr in the whole document
//td[2]Second td within each row

XPath axes: move around the DOM

Axes are XPath’s superpower: they let you find an element relative to another, which CSS cannot do upwards.

AxisMoves to
parent::tag or ..The parent element
ancestor::tagAny ancestor (parent, grandparent…)
following-sibling::tagSiblings after the element
preceding-sibling::tagSiblings before the element
descendant::tagAny element inside

๐Ÿ“Š Real Example: Tables with CSS, XPath and Filters

The practice page https://the-internet.herokuapp.com/tables has a table #table1 with the columns Last Name, First Name, Email, Due, Web Site and Action. Let’s read Jason Doe’s amount due ($100.00) three different ways.

import { test, expect } from '@playwright/test';

test.describe('read a table cell', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://the-internet.herokuapp.com/tables');
  });

  test('with CSS nth-child', async ({ page }) => {
    // 3rd row, 4th column (Due)
    const due = page.locator('#table1 tbody tr:nth-child(3) td:nth-child(4)');
    await expect(due).toHaveText('$100.00');
  });

  test('with XPath following-sibling', async ({ page }) => {
    // From the "Jason" cell, the 2nd cell after it is "Due"
    const due = page.locator("//table[@id='table1']//td[text()='Jason']/following-sibling::td[2]");
    await expect(due).toHaveText('$100.00');
  });

  test('with Playwright filter (recommended)', async ({ page }) => {
    const row = page.locator('#table1 tbody tr').filter({ hasText: 'Jason' });
    await expect(row.locator('td').nth(3)).toHaveText('$100.00');
    await expect(page.locator('#table1 tbody tr')).toHaveCount(4);
  });

  test('click edit in the row of a user (XPath parent)', async ({ page }) => {
    await page.locator("//table[@id='table1']//td[text()='Doe']/parent::tr//a[text()='edit']").click();
    await expect(page).toHaveURL(/#edit/);
  });
});

The filter version is the easiest to read: it finds the row by meaningful text instead of a hard-coded row number, so it keeps working when rows are re-ordered or new rows are added. For more table techniques see reading data from web tables.


๐Ÿ”— Combine Selectors with Playwright Locator Methods

You do not have to put everything in one long selector. Chain and filter instead:

// Chain: find inside another locator
const form = page.locator('form');
await form.locator('#user-name').fill('standard_user');

// Filter by text or by a child element
page.locator('#table1 tbody tr').filter({ hasText: 'Conway' });
page.locator('tr').filter({ has: page.locator('a', { hasText: 'delete' }) });

// Pick from several matches
page.locator('#table1 tbody tr').first();
page.locator('#table1 tbody tr').last();
page.locator('#table1 tbody tr').nth(1);   // zero-based

// Mix CSS and role locators
page.locator('#table1').getByRole('link', { name: 'edit' }).first();

โš ๏ธ Common mistake: Playwright locators are strict: an action like click() fails if the selector matches more than one element. Fix it by making the selector more specific or by using filter, first() or nth() deliberately, not by habit.


๐Ÿงช Test Your Selectors Before Using Them

  • Browser DevTools console: document.querySelectorAll('#table1 tbody tr') for CSS, $x("//td[text()='Jason']") for XPath (Chrome).
  • DevTools Elements panel: press Ctrl + F and type a CSS selector or XPath to highlight matches.
  • Playwright Inspector / VS Code “Pick locator”: click an element and Playwright suggests the best locator. See Playwright Codegen.
  • UI mode: npx playwright test --ui lets you try locators against a live snapshot.

โš–๏ธ CSS vs XPath: Which Should You Use?

CSSXPath
ReadabilityShort and familiarLonger
Match by textNot in standard CSS (use filter or getByText)Yes, text() and contains()
Move to parent or ancestorNoYes, with axes
Shadow DOMPlaywright CSS pierces open shadow DOMDoes not pierce shadow DOM
Typical useids, attributes, structuretext plus structure, relative navigation

Rule of thumb: use role-based locators first, CSS for ids and test attributes, and XPath only when you need text-based or upward navigation that filters cannot express.


๐Ÿšซ Selectors to Avoid

  • Absolute XPath such as /html/body/div[2]/div/form/div[1]/input: breaks with any layout change.
  • Long CSS chains such as div > div > div:nth-child(3) > span.
  • Auto-generated ids and classes (#ember123, .css-1kq9xd).
  • Index-only selectors when a stable text or attribute exists.

๐Ÿ“ Assignment

Use https://www.saucedemo.com and https://the-internet.herokuapp.com/tables:

  1. Write three different CSS selectors for the Sauce Demo password field and use one in a test.
  2. Write an XPath that finds the login button using both @type and @value.
  3. On the tables page, assert that the email of Frank Bach is fbach@yahoo.com using XPath with following-sibling.
  4. Do the same with filter({ hasText }) and nth().
  5. Count the rows of #table1 and assert there are 4.
  6. Click the delete link in Tim Conway’s row using an XPath with parent::tr.
  7. Bonus: rewrite every selector from tasks 1 to 6 using role-based locators where possible, and note which ones were not possible.

โ“ FAQ

๐Ÿค” How do I use XPath in Playwright?

Pass it to page.locator(): page.locator("//input[@id='user-name']"). Selectors starting with // are detected as XPath automatically, or prefix them with xpath=.

๐Ÿค” How do I use a CSS selector in Playwright?

page.locator('#user-name') or page.locator('[data-test="password"]'). CSS is the default selector engine for locator().

๐Ÿค” Is XPath slower than CSS in Playwright?

The difference is negligible in practice. Choose based on readability and stability; prefer role-based locators and CSS, and use XPath when you need its axes or text matching.

๐Ÿค” Why does XPath not find my element inside a web component?

XPath does not pierce shadow DOM. Use a CSS selector or a role/text locator, which Playwright resolves inside open shadow roots.

๐Ÿค” How do I select by text with CSS in Playwright?

Standard CSS cannot match text. Use getByText(), or locator('tr').filter({ hasText: 'Jason' }).


๐ŸŽฏ Next Steps

๐ŸŽฌ Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

1 thought on “CSS and XPath Selectors in Playwright: Complete Guide with Examples”

Leave a Comment