Subscribe Watch on YouTube

Reference

The Locator Cheat Sheet.

The locators you choose decide how stable your suite is. Here is every strategy at a glance, with copy-ready examples and the rules that keep tests from flaking. Click any selector to copy it.

Read the full guide

Pick in this order

This is the fallback ladder for a raw CSS or XPath selector. Start at one, and only move down when the level above does not exist and cannot be added. If your test tool has role or text queries (see the modern order below), start there instead.

1 [data-testid="submit"] Dedicated test IDs never change by accident. Ask your developers for them; most will say yes.
2 #submit-button IDs are unique and fast, when they exist and are not auto-generated.
3 [name="email"] Form fields almost always have stable name attributes.
4 button.btn-primary Tag plus a meaningful class. Fine when classes describe purpose, fragile when they describe styling.
5 button=Submit order Text selectors (WebdriverIO syntax) read like the user thinks, but break when copy changes.
6 //div[@class="cart"]//button[2] XPath with position is the last resort. One layout change and it points at the wrong element.

The modern order: locate like a person

The raw CSS and XPath below are the foundation. Testing Library and Playwright both suggest reaching first for roles and accessible names, the same things a screen reader exposes. Work down this list and your tests resemble how people actually use the page.

1 getByRole('button', { name: 'Save' }) By role + name. Matches what a person, and a screen reader, perceives. Start here for almost everything.
2 getByLabel('Email address') By label. Resolves the link between a label and its field, the way assistive tech does.
3 getByPlaceholder('Search') By placeholder. A fallback for fields that have no associated label.
4 getByText('Add to cart') By text. Content a person can read on the page.
5 getByAltText('Company logo') By alt or title. Images and titled elements. Semantic, but narrower.
6 getByTestId('checkout-cta') By test id. The fallback. Use it only when role or text cannot identify the element, or the text is dynamic.

Implicit ARIA roles

What getByRole('…') resolves to. Semantic HTML gives you these for free.

<button> button
<a href> link
<input type="text"> textbox
<input type="search"> searchbox
<input type="checkbox"> checkbox
<input type="radio"> radio
<select> combobox
<h1> to <h6> heading (levels 1 to 6)
<ul> / <ol> list
<li> listitem
<table> table
<tr> row
<td> cell
<th scope="col"> columnheader
<nav> navigation
<img alt="…"> img

Accessible name

The name in getByRole(role, { name }) is computed in this order of precedence (W3C accname):

  • 1aria-labelledby, the ids of elements that label this one
  • 2aria-label, a literal string on the element
  • 3an associated <label> for form fields
  • 4the element's own text content
  • 5placeholder or title, as a last fallback

That is why role plus name keeps working after a refactor: it depends on meaning, not the exact markup.

CSS selectors

#id By ID
.class By class
input[name="email"] By attribute
input[placeholder*="Search"] Attribute contains
a[href^="https"] Attribute starts with
img[src$=".png"] Attribute ends with
.parent > .child Direct child
.list li:first-child First in a list
.list li:nth-child(3) Third in a list
.a, .b Either of two

XPath patterns

//button[text()="Save"] Exact text
//button[contains(text(), "Save")] Text contains
//input[@name="email"] By attribute
//div[contains(@class, "error")] Class contains
//label[text()="Email"]/following-sibling::input Sibling of a label
//tr[td[text()="Julia"]] Row containing a cell
//ul[@id="cart"]/li[last()] Last item

WebdriverIO shorthand

await $('[data-testid="submit"]') Single element
await $$('.cart-item') All matching elements
await $('button=Submit') Exact link/button text
await $('button*=Sub') Partial text
await $('android=new UiSelector().text("Login")') Android (Appium)

Do

  • Prefer attributes that describe purpose, not appearance
  • Ask for data-testid attributes on anything you automate
  • Keep every selector in one place (page objects), never inline in tests
  • Test your selector in dev tools before writing the test
  • Use the shortest selector that is still unique

Don't

  • Don't chain long paths: .a > div > div:nth-child(2) > span
  • Don't select by index unless order is the thing you're testing
  • Don't use auto-generated classes (css-1x2y3z) or IDs (ember-123)
  • Don't copy the full XPath from dev tools; it breaks on the next layout change.
  • Don't select by visible copy that marketing can change

From foundation to framework

Selectors are the foundation. Every modern tool adds its own conveniences on top, such as auto-waiting, scoping and accessible-name queries. Same idea, different surface. Pick a tool to see how each one builds on the basics.

Playwright

Locate

getByRole, getByLabel, getByText and getByTestId, plus page.locator() for CSS and XPath.

Best practice

Prefer role and text locators. CSS and XPath are fallbacks. Locators are strict, so they throw if more than one element matches.

Beyond selectors

Auto-waits for actionability. Narrow with .filter({ has, hasText }), or combine with .and() and .or().

Tricky cases

Four situations where a plain selector needs help, and what each tool does about it.

Shadow DOM

CSS can reach into an open shadow root; XPath cannot. Playwright and WebdriverIO pierce it for you, Cypress needs extra setup.

Inside an iframe

A selector only sees its own document. Switch in first: Playwright frameLocator(), WebdriverIO switchToFrame(), Cypress via the iframe plugin.

Text that changes

Copy edits and translations break text selectors. For anything that gets translated, prefer a role with a stable name, or a test id.

One match at a time

Most actions expect a single element. Playwright treats more than one match as an error, which is useful: it flags a selector that is too broad.

Practice

The Locator Playground.

Read the task, write a selector, then run it. The tool highlights what your selector matches in the sample app, tells you whether you hit the right element, grades how resilient it is, and shows how Playwright, Cypress and WebdriverIO would target the same thing. There is usually more than one correct answer. Anything that matches counts.

shopmate.test
Spring sale: 20% off keyboards
  • Wireless Mouse

    $24
  • Mechanical Keyboard

    $89
  • USB-C Hub

    $39
Challenge 1 / 34 Easy

Loading…

How the frameworks target this CSS and XPath are the foundation. This is the modern way.
Playwright
Cypress
WebdriverIO

The page only shows the visible UI. To see the attributes, roles and test ids you write selectors against, right-click an element in this app and choose Inspect (or press F12) to open your browser dev tools.