---
title: 'Locators: Find Elements the Way Users Do | TestingBot'
description: 'A locator is a description of how to find an element, not the element
  itself. `page.getByRole(&#39;button&#39;, { name: &#39;Save&#39; })` does nothing
  on…'
source_url:
  html: https://testingbot.com/resources/courses/playwright/locators
  md: https://testingbot.com/resources/courses/playwright/locators.md
---

Course

[Playwright Tutorial: Zero to CI](https://testingbot.com/resources/courses/playwright)

Chapter 3 of 10

1. [1 Playwright Tutorial for Beginners](https://testingbot.com/resources/courses/playwright)
2. [2 Install Playwright and Run a First Test](https://testingbot.com/resources/courses/playwright/install-and-first-test)
3. [3 Locators: Find Elements the Way Users Do](https://testingbot.com/resources/courses/playwright/locators)
4. [4 Auto-Waiting and Web-First Assertions](https://testingbot.com/resources/courses/playwright/auto-waiting-and-assertions)
5. [5 Fixtures, Hooks and Logging In Once](https://testingbot.com/resources/courses/playwright/fixtures-hooks-and-authentication)
6. [6 Debugging: UI Mode, Trace Viewer, Codegen](https://testingbot.com/resources/courses/playwright/debugging-tests)
7. [7 Multiple Browsers, Workers and Sharding](https://testingbot.com/resources/courses/playwright/browsers-parallelism-and-sharding)
8. [8 Run Playwright in CI with GitHub Actions](https://testingbot.com/resources/courses/playwright/github-actions-ci)
9. [9 Run Your Playwright Suite on a Cloud Grid](https://testingbot.com/resources/courses/playwright/run-on-a-cloud-grid)
10. [10 Knowledge check](https://testingbot.com/resources/courses/playwright/knowledge-check)

[Exit course](https://testingbot.com/resources/courses)

 Chapter 3 of 10 Beginner 7 min read 2 quick checks Updated Nov 2026 

# Locators: Find Elements the Way Users Do
On this page
1. [The built-in locators, in order of preference](https://testingbot.com#the-built-in-locators-in-order-of-preference)
2. [Strictness](https://testingbot.com#strictness)
3. [Chaining and filtering](https://testingbot.com#chaining-and-filtering)
4. [Finding locators without guessing](https://testingbot.com#finding-locators-without-guessing)
5. [Where locators should live](https://testingbot.com#where-locators-should-live)

A locator is a description of how to find an element, not the element itself. `page.getByRole('button', { name: 'Save' })` does nothing on its own; Playwright resolves it against the live page every time you act on it or assert on it. That has two consequences that shape everything in this chapter:

1. A locator never goes stale. If React re-renders the button, the next `click()` simply finds the new one.
2. Because resolution happens at action time, Playwright can wait and retry while it resolves, which is where auto-waiting comes from (chapter 3).

## The built-in locators, in order of preference[#](https://testingbot.com#the-built-in-locators-in-order-of-preference)

Playwright's own guidance is to locate elements by what a user or assistive technology perceives, and to fall back to implementation details only when you must:

| Locator | Finds | Example |
| --- | --- | --- |
| `getByRole` | elements by ARIA role and accessible name | `getByRole('button', { name: 'Sign in' })` |
| `getByLabel` | form controls by their label text | `getByLabel('Password')` |
| `getByPlaceholder` | inputs by placeholder | `getByPlaceholder('What needs to be done?')` |
| `getByText` | non-interactive elements by text | `getByText('2 items left')` |
| `getByAltText` | images by alt text | `getByAltText('Company logo')` |
| `getByTitle` | elements by `title` attribute | `getByTitle('Close')` |
| `getByTestId` | elements by `data-testid` | `getByTestId('todo-item')` |

`getByRole` should be your default. It matches implicit roles (a `<button>` has role `button`, an `<a href>` has role `link`, an `<input type="checkbox">` has role `checkbox`) as well as explicit `role` attributes, and the `name` option matches the accessible name: the visible text, the associated label, or `aria-label`. A test written with role locators keeps passing through a CSS refactor and fails when the button really stops being a button, which is the failure you want to hear about. As a side effect, if you cannot write a role locator for a control, that control probably has an accessibility problem.

Some options worth knowing:

```typescript
page.getByRole('heading', { name: 'Secure Area', exact: true }); // exact, case-sensitive
page.getByRole('heading', { level: 1 });
page.getByRole('checkbox', { checked: true });
page.getByRole('link', { name: /^completed$/i }); // regular expression
```

By default `name` matching is case-insensitive and matches a substring, so `{ name: 'Save' }` also matches "Save draft". Add `exact: true` or use an anchored regex when that matters.

`getByTestId` is the right choice when there is no meaningful user-facing hook, for example a list row whose text changes with data. If your app uses a different attribute, set it once in the config with `use: { testIdAttribute: 'data-test' }`.

Raw CSS and XPath still work through `page.locator('css=...')` or `page.locator('//xpath')`, but treat them as the escape hatch. They couple the test to markup that designers and developers change freely.

`page.getByRole('button', { name: 'Save' })` matches both a Save button and a Save draft button. What is the cleanest fix?

 Add `exact: true` to the options Append `.first()` Switch to `page.locator('.btn-save')` Use `getByText('Save')` instead

Name matching is a case-insensitive substring by default, so Save also matches Save draft. `exact: true` (or an anchored regex) says what you mean; `.first()` depends on ordering and a CSS class couples the test to markup.

## Strictness[#](https://testingbot.com#strictness)

Every action on a locator requires it to resolve to exactly one element. On the TodoMVC demo, each row has a checkbox labelled "Toggle Todo". With two todos in the list, this fails:

```typescript
await page.getByRole('checkbox', { name: 'Toggle Todo' }).check();
```

The error is unusually helpful:

```
Error: strict mode violation: getByRole('checkbox', { name: 'Toggle Todo' }) resolved to 2 elements:
    1) <input class="toggle" type="checkbox" aria-label="Toggle Todo"/> aka getByRole('checkbox', { name: 'Toggle Todo' }).first()
    2) <input class="toggle" type="checkbox" aria-label="Toggle Todo"/> aka getByRole('listitem').filter({ hasText: 'b' }).getByLabel('Toggle Todo')
```

Strictness is a feature. In Selenium, `findElement` quietly returns the first match and your test clicks the wrong row. Playwright makes you say which one you mean. The suggested `.first()` fixes the error but is usually the wrong fix, because "first" is an accident of ordering. The second suggestion, narrowing by the row's content, says what you mean.

Assertions that are about many elements, such as `toHaveCount` or `toHaveText([...])`, are allowed to match multiple elements.

## Chaining and filtering[#](https://testingbot.com#chaining-and-filtering)

You narrow a locator by chaining it from another locator, and by filtering:

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

test.beforeEach(async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/');
});

test('completes one todo out of three', async ({ page }) => {
  const newTodo = page.getByPlaceholder('What needs to be done?');
  for (const item of ['Buy milk', 'Walk the dog', 'Pay rent']) {
    await newTodo.fill(item);
    await newTodo.press('Enter');
  }

  const items = page.getByTestId('todo-item');
  await expect(items).toHaveCount(3);

  // Narrow to one row by its text, then search inside that row only
  const dogRow = items.filter({ hasText: 'Walk the dog' });
  await dogRow.getByRole('checkbox', { name: 'Toggle Todo' }).check();
  await expect(dogRow).toHaveClass(/completed/);

  await page.getByRole('link', { name: 'Completed' }).click();
  await expect(items).toHaveText(['Walk the dog']);
  await expect(page.getByTestId('todo-count')).toHaveText('2 items left');
});
```

The pattern `rows.filter({ hasText }).getByRole(...)` is the one you will write most often: find the container that identifies the thing, then find the control inside it. `filter` accepts four options:

```typescript
items.filter({ hasText: 'Walk the dog' }); // contains text
items.filter({ hasNotText: 'Buy milk' }); // does not contain text
items.filter({ has: page.getByRole('checkbox', { checked: true }) }); // contains a matching element
items.filter({ hasNot: page.getByRole('checkbox', { checked: true }) });
```

`has` and `hasNot` take another locator, which is evaluated relative to each candidate row, not to the page.

Two combinators cover the remaining cases:

```typescript
// both conditions on the same element
const saveButton = page.getByRole('button').and(page.getByTitle('Save changes'));

// either element, for example an optional dialog that may appear first
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog).first()).toBeVisible();
```

Use `.or()` sparingly: if both sides can be present at the same time it can trip strictness, which is why the example adds `.first()`.

Positional helpers, `.first()`, `.last()` and `.nth(i)`, exist and are fine when position really is the point ("the newest message is at the top"). Otherwise prefer a filter.

In `items.filter({ has: page.getByRole('checkbox', { checked: true }) })`, what is the inner locator evaluated against?

 The whole page, once, before filtering Each candidate item, so only rows that contain a checked checkbox remain The first item only

`has` and `hasNot` evaluate the inner locator relative to each candidate, even though it is written starting from `page`. It is not a page-wide check.

## Finding locators without guessing[#](https://testingbot.com#finding-locators-without-guessing)

You do not have to work these out by reading HTML:

- In UI mode (`npx playwright test --ui`), the **Pick locator** button lets you hover over the page snapshot and shows the locator Playwright would use.
- `npx playwright codegen <url>` generates role-based locators as you click around.
- The VS Code extension has the same picker inside the editor.

Treat their output as a suggestion. A generated `getByRole('link', { name: 'Walk the dog' })` might be relying on text that comes from test data; a filter on the row may be more stable.

## Where locators should live[#](https://testingbot.com#where-locators-should-live)

When the same locator appears in ten tests, it belongs in one place. The common answer is a page object: a class that owns the locators and the actions for one page or component. This course stays with inline locators to keep each example self-contained, and chapter 4 shows the fixture that hands a page object to your tests. For the pattern itself, see our [Page Object Model course](https://testingbot.com/resources/courses/page-object-model).

For the full reference, including frames and shadow DOM (locators pierce open shadow roots by default; XPath does not), see the [official locators guide](https://playwright.dev/docs/locators). Our [Playwright documentation](https://testingbot.com/support/web-automate/playwright) covers how the same locators behave on remote browsers.

[Previous · Chapter 2 Install Playwright and Run a First Test](https://testingbot.com/resources/courses/playwright/install-and-first-test) [Next · Chapter 4 Auto-Waiting and Web-First Assertions](https://testingbot.com/resources/courses/playwright/auto-waiting-and-assertions)

On this page

1. [The built-in locators, in order of preference](https://testingbot.com#the-built-in-locators-in-order-of-preference)
2. [Strictness](https://testingbot.com#strictness)
3. [Chaining and filtering](https://testingbot.com#chaining-and-filtering)
4. [Finding locators without guessing](https://testingbot.com#finding-locators-without-guessing)
5. [Where locators should live](https://testingbot.com#where-locators-should-live)

Certification

This course has a free, hands-on certificate.

[How it works](https://testingbot.com/resources/courses/playwright/certification)
