THE SHORT VERSION
Choose the part of the UI your test should depend on. Start with getByRole for a control’s role and accessible name. Use getByTestId for a maintained test identity, or CSS for a deliberate markup dependency.
Each option still needs the right scope and an assertion for the result of the action.
Compare the dependency, not just the syntax.
These are three ways to create a Playwright locator. The useful difference is what must remain true about the element for your locator to keep finding it.
| Locator | A good fit when | Depends on | Review when |
|---|---|---|---|
getByRole | The test should identify a control the way a user does. | The element’s role and accessible name. | The role, name, or number of matching controls changes. |
getByTestId | The action needs a maintained identity independent of its label. | An agreed test ID and the configured attribute. | The ID is removed, renamed, or repeated within the scope. |
CSS selector | A stable markup attribute or relationship is the right target. | The specific attributes, tags, or structure in the selector. | A selected attribute, ancestor, or position changes. |
On a small screen, scroll the table to compare all four columns.
Playwright recommends user-facing locators and explicit testing contracts. See its locator best practices.
The same button, located three ways.
Switch between the examples and inspect what each one selects. The snippets use the same sample markup, so the tradeoff is easy to compare.
The button stays the same. Change how the test finds it.
Illustrative markup · no checkout or payment
See the button’s HTML
<button
type="button"
data-testid="checkout-submit"
data-action="place-order"
>
Place order
</button>Find the button by its role and the accessible name “Place order.” In this markup, the name comes from the button’s text.
const submit = page.getByRole('button', {
name: 'Place order',
exact: true,
});
await expect(submit).toHaveCount(1);
await submit.click();Use inside a test with page ready and expect imported from @playwright/test. These snippets identify and click the button; add your assertion for the expected outcome.
If the accessible name changes to “Complete purchase,” update this locator only if that change is intended.
The example shows code; it does not run Playwright in your browser. Explore what happens when the sample UI changes.
Decide which changes the test should notice.
When the control’s meaning matters, use its role and name.
A role locator describes a button as a button. Its name may come from visible text, a label, or an ARIA naming attribute. Inspect the accessible name rather than assuming it always matches the text on screen.
If a checkout button is renamed, decide whether that is a product change to accept or a regression the test should catch. A locator failure can be useful information.
Role locator APIWhen identity should outlast the label, agree on a test ID.
A checkout action may appear under different labels across locales or experiments. A maintained ID can identify that action independently. Keep a separate assertion when the label or role is part of the behavior you need to verify.
The default attribute is data-testid. Naming and ownership still matter: an ID that changes with every refactor is not a useful agreement.
When markup is the requirement, keep the CSS dependency small.
The sample’s data-action selector relies on a specific attribute and a button tag. It does not rely on the button’s styling class, its wrapper, or which child it is. Those are separate choices, not inevitable parts of using CSS.
When only structural markup is available, review each relationship you include. A selector tied to several ancestors has more ways to change during a redesign.
CSS locator guidanceAlready using a different test ID attribute?
Configure the attribute once. With this setting, getByTestId('checkout-submit') targets data-qa="checkout-submit" in your app.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: { testIdAttribute: 'data-qa' },
});Two Save buttons need a more specific question.
“Find Save” is ambiguous when both Shipping and Billing contain that action. Identify the part of the page first, then the control within it.
<section aria-label="Shipping">
<button type="button">Save</button>
</section>
<section aria-label="Billing">
<button type="button">Save</button>
</section>const billing = page.getByRole('region', {
name: 'Billing',
exact: true,
});
const save = billing.getByRole('button', {
name: 'Save',
exact: true,
});
await expect(save).toHaveCount(1);
await save.click();The named section gives the Billing area a region role with an accessible name. The locator then finds its Save button, even if the two sections swap positions.
The same idea applies to repeated test IDs: identify a meaningful container first. Use first() or nth() only when position itself is intentional; changing the index should not silently choose a different task.
See Playwright’s scoped locator examples and strictness rules.
A matching element is the start of the test.
All three methods create locators with Playwright’s action behavior. A click waits for its required actionability checks; it does not establish that an order was saved or the right page appeared. Assert that result explicitly.
See the auto-waiting and actionability reference.
- The locator points to the intended element in the state the test uses.
- Its name, test ID, or markup dependency is a deliberate choice.
- Repeated controls are narrowed to a meaningful part of the page.
- The test checks the expected result after the action.
- A failure is reviewed for page state, data, and timing before changing the locator.
If the failure only occurs after earlier actions, keep that sequence with the report. The frontend bug reproduction guide helps you preserve the setup and steps.
FROM THE EXAMPLE TO YOUR APP
Choose with the element in front of you.
Inspect an element and review its selectors with our extension. Copy a candidate from the live page, then verify it in the state your test uses.
Free extension for desktop Chrome. Try it before signing up.The extension’s current Playwright snippet uses CSS with page.locator(). The role and test ID examples in this guide are code you can adapt.
A few common questions.
Which locator should I try first in Playwright?
Start with a user-facing locator such as getByRole when the control’s role and accessible name express the interaction. Use a maintained test ID when a separate test identity fits better, or CSS when you deliberately need a markup attribute or relationship. Check the actual target in each case.
Are CSS selectors always brittle?
No. A selector based on a maintained attribute has different dependencies from one based on generated classes or nested positions. Review what the selector depends on and whether that dependency is part of your team’s intended contract.
Is getByTestId the same as selecting data-testid with CSS?
For a simple element using Playwright’s default configuration, both can identify the same data-testid value. getByTestId expresses the testing convention directly and follows the configured testIdAttribute. A literal CSS attribute selector continues to use whichever attribute you wrote.
Does getByRole check accessibility?
It uses the role and accessible name, so it can expose some problems with how a control is identified. A matching role locator does not establish that the page is accessible. Keep dedicated accessibility checks alongside your functional tests.
Do CSS locators also auto-wait?
Yes. page.locator(), getByRole(), and getByTestId() all create Playwright locators. An action such as click applies its actionability checks regardless of which of these methods created the locator. Auto-waiting does not prove the application reached the correct business result.
Can I combine test IDs and role locators?
Yes. For example, identify a specific panel by its test ID, then find the Save button within it by role and accessible name. Choose a scope that expresses the intended area rather than relying on the order of matching buttons.
Sources & further reading
API behavior is checked against Playwright’s documentation. The checkout and settings markup are authored examples for this guide.