Choosing Playwright Locators: User-Facing vs CSS Selectors
Guidance on selecting Playwright locator strategies to reduce test flakiness while maintaining accessibility.
13 Sept 2026, 04:51 UTC

Decision: Locator Strategy for Stable Playwright Tests
The most common source of flaky end-to-end tests is coupling test logic to implementation details such as generated class names or deep HTML hierarchies. When a UI redesign changes those details, tests fail even though the user-visible behavior is unchanged. The decision is whether to anchor tests to how a user perceives the page (user-facing locators) or to the low-level markup (CSS selectors). The useful takeaway: start with user-facing locators for resilience and accessibility, and fall back to CSS selectors only when the required semantics are missing.
Comparison of Locator Strategies
| Strategy | Primary Playwright API | Resilience to DOM changes | Precision for complex layouts | Typical use case |
|---|---|---|---|---|
| User-facing | getByRole(), getByText(), getByLabel() |
High | Medium | Standard controls, forms, navigation |
| CSS selector | locator('.class'), locator('#id'), locator('div > span') |
Low | High | Dynamic grids, canvas-based widgets, non-semantic markup |
Trade-offs and Constraints
User-facing locators
These APIs query the accessibility tree, mirroring what a screen reader announces. By using getByRole you implicitly verify that the element has an accessible name and role, which improves test coverage of accessibility concerns. A trade-off appears with getByText when exact string matching is used: if the application translates UI strings, the test will fail for other locales unless you use a regex or the ignoreCase option.
CSS selectors
CSS selectors can pinpoint an element even when it lacks ARIA attributes, giving high precision for intricate layouts. However, they bind the test to the exact class names, IDs, or nesting depth. A redesign that adopts a utility-first CSS framework or renames utility classes will break the selector, increasing maintenance cost. Deeply nested selectors also create tight coupling to the HTML hierarchy, making the test brittle to any structural refactor.
Implementation Example: Refining a List Item with filter()
To avoid index-based CSS when you need to act on a specific row in a table, first locate the row by a user-facing trait, then narrow to the button inside it.
// file: tests/example.spec.ts
import { test, expect } from '@playwright/test';
test('delete a user from the table', async ({ page }) => {
await page.goto('https://example.app/users');
// 1. Find the table row that shows the user's name
const row = page.getByRole('row', { hasText: 'Ada Lovelace' });
// 2. Within that row, locate the delete button by its accessible name
const deleteBtn = row.getByRole('button', { name: /delete/i });
// 3. Perform the action and verify the row disappears
await deleteBtn.click();
await expect(row).toBeHidden();
});
Verification and Diagnostics
Run the Playwright code generator against a page to see which locator type it suggests first:
# Replace with your application URL npx playwright codegen https://example.app/users
If the generator proposes a lengthy CSS path, consider adding appropriate ARIA roles or labels to the markup.
To confirm that a test is resilient to class-name changes, rename a CSS class used only for styling (e.g., .btn-primary → .btn-main) and run the test suite. Tests that rely on getByRole or getByText should continue to pass, whereas tests that use the renamed class in a CSS selector will fail.
Limitations and Practical Check
User-facing locators require the element to be exposed in the accessibility tree. If you are working with a legacy application that lacks ARIA roles, labels, or meaningful text, the locator may return null. In such cases, a CSS selector can be a temporary workaround while you improve the markup.
Practical way to verify the chosen strategy: after adding an ARIA role or label to a component, run the related test. If the test begins to pass with a user-facing locator where it previously failed, you have confirmed that the locator now correctly targets the element.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.