Playwright Locators: Complete Guide with Examples
If Playwright automation is a car, locators are the steering wheel.
A Playwright test first needs to find a web element before it can click, fill, check, hover, select, upload, or verify anything.
That is why learning Playwright Locators is one of the most important steps for anyone learning Playwright automation testing.
Table of Contents
ToggleA locator tells Playwright:
“Find this particular element on the webpage.”
For example:
await page.getByRole(‘button’, { name: ‘Login’ }).click();
This tells Playwright to find a button whose accessible name is Login and click it.
Playwright’s official documentation recommends user-facing locators such as roles, labels, text, placeholders, and explicit test IDs because they can make tests easier to understand and maintain. Locators also provide Playwright’s auto-waiting and retry behavior.
This guide explains Playwright locators from beginner to advanced level, including practical examples, locator selection rules, CSS and XPath, chaining, filtering, strict mode, dynamic elements, Codegen, debugging, and real-world best practices.
What Are Playwright Locators?
Playwright locators are APIs used to find web elements on a webpage so that Playwright can interact with or verify those elements.
Common Playwright locators include:
getByRole()
getByText()
getByLabel()
getByPlaceholder()
getByAltText()
getByTitle()
getByTestId()
locator()
Playwright also supports CSS and XPath selectors through locator().
A simple example is:
await page.getByRole(‘button’, { name: ‘Login’ }).click();
The locator finds the Login button, and click() performs the action.
What Is a Locator in Playwright?
Imagine a webpage contains 50 elements.
You tell Playwright:
“Click the Login button.”
Playwright needs a way to identify which element you mean.
That identification method is a locator.
For example:
const loginButton = page.getByRole(‘button’, { name: ‘Login’ });
await loginButton.click();
There are two important ideas here:
- getByRole() describes what element you want.
- click() describes what you want to do with it.
This makes Playwright tests easier to read.
Why Are Playwright Locators Important?
Locators are important because almost every UI automation task starts by finding an element.
You may need to:
- Click a button.
- Enter a username.
- Enter a password.
- Select a checkbox.
- Choose a dropdown option.
- Upload a file.
- Verify a message.
- Open a menu.
- Check a heading.
- Verify a table row.
- Interact with a dialog.
- Work with an iframe.
All of these tasks require Playwright to identify the correct element.
A good locator can make a test:
- Easier to read.
- Easier to debug.
- More resistant to UI changes.
- Less dependent on HTML structure.
- Easier for another tester to understand.
A poor locator can make tests:
- Difficult to maintain.
- Sensitive to UI redesigns.
- Ambiguous.
- Flaky.
- Hard to debug.
Playwright Locator Types
The main built-in locator methods are:
Locator | Best Use |
getByRole() | Buttons, links, headings, checkboxes and other user-facing elements |
getByLabel() | Form fields with labels |
getByText() | Visible non-interactive text |
getByPlaceholder() | Inputs identified by placeholder |
getByAltText() | Images and elements with alternative text |
getByTitle() | Elements with a title attribute |
getByTestId() | Explicit testing contracts |
locator() | CSS, XPath and more specialized selection |
Playwright recommends prioritizing user-facing locators and explicit testing contracts rather than automatically reaching for long CSS or XPath expressions.
1. Playwright getByRole()
getByRole() is one of the most useful Playwright locator methods.
It finds an element according to its ARIA role and accessible name.
Example:
await page.getByRole(‘button’, { name: ‘Login’ }).click();
The role is:
button
The accessible name is:
Login
Example HTML
<button>Login</button>
Playwright can identify it with:
page.getByRole(‘button’, { name: ‘Login’ })
More Examples
Button
await page.getByRole(‘button’, { name: ‘Submit’ }).click();
Link
await page.getByRole(‘link’, { name: ‘Contact Us’ }).click();
Heading
await expect(
page.getByRole(‘heading’, { name: ‘Dashboard’ })
).toBeVisible();
Checkbox
await page.getByRole(‘checkbox’, { name: ‘Accept Terms’ }).check();
Radio button
await page.getByRole(‘radio’, { name: ‘Male’ }).check();
Dialog
const dialog = page.getByRole(‘dialog’, { name: ‘Login’ });
Playwright recommends role locators because they are close to how users and assistive technologies perceive interactive elements.
When should you use getByRole()?
Use it when an element has a meaningful role and accessible name.
For example:
page.getByRole(‘button’, { name: ‘Save’ })
is generally easier to understand than:
page.locator(‘#saveBtn’)
2. Playwright getByLabel()
getByLabel() is designed mainly for form controls associated with labels.
Suppose your HTML is:
<label>
<input type=”email”>
</label>
You can write:
await page.getByLabel(‘Email’).fill(‘user@example.com’);
Another common HTML structure is:
<label for=”username”>Username</label>
<input id=”username”>
The locator can still be:
await page.getByLabel(‘Username’).fill(‘john’);
Why is getByLabel() useful?
A form normally tells users what each field means.
For example:
Username
Password
Phone Number
Address
Using those labels makes the test easy to understand.
Example Login
await page.getByLabel(‘Username’).fill(‘john123’);
await page.getByLabel(‘Password’).fill(‘mypassword’);
await page.getByRole(‘button’, { name: ‘Login’ }).click();
This reads almost like a manual test case.
Playwright’s documentation specifically recommends label locators for form controls with associated labels.
3. Playwright getByText()
getByText() finds an element using its visible text.
Example:
await expect(
page.getByText(‘Login successful’)
).toBeVisible();
Suppose the webpage contains:
<div>Payment successful</div>
You can write:
await expect(
page.getByText(‘Payment successful’)
).toBeVisible();
Exact Text Matching
If several elements contain similar text, use exact matching:
await page.getByText(‘Save’, { exact: true }).click();
Regular Expressions
You can also use a regular expression:
await expect(
page.getByText(/payment successful/i)
).toBeVisible();
Playwright normalizes whitespace during text matching, and its documentation supports substring, exact, and regular-expression matching.
When should you use getByText()?
Use it when the visible text itself is the meaningful identifier.
For example:
Order placed successfully
Payment completed
Welcome, John
No products found
For interactive controls such as buttons and links, prefer getByRole() when it provides a clear locator.
4. Playwright getByPlaceholder()
Many input fields contain placeholder text.
Example:
<input placeholder=”Enter your email”>
You can locate it using:
await page
.getByPlaceholder(‘Enter your email’)
.fill(‘john@example.com’);
Another example:
await page.getByPlaceholder(‘Search products’).fill(‘Laptop’);
Important Limitation
A placeholder is not always the best long-term locator.
Designers may change:
Enter your email
to:
Email address
because of UI or marketing changes.
Therefore, if a proper label exists, getByLabel() is often a better choice.
5. Playwright getByAltText()
Images can have alternative text.
Example:
<img alt=”Playwright logo” src=”logo.png”>
You can locate the image using:
await page.getByAltText(‘Playwright logo’).click();
This locator is useful for elements such as images that expose meaningful alternative text.
Playwright officially documents getByAltText() for elements supporting alternative text, such as img and area.
6. Playwright getByTitle()
If an element has a title attribute, you can use:
await page.getByTitle(‘Settings’).click();
Example:
<button title=”Settings”>
⚙
</button>
Locator:
page.getByTitle(‘Settings’)
This is useful when the title is a stable and meaningful attribute.
7. Playwright getByTestId()
A test ID is an attribute specifically designed to help automated tests identify an element.
Example:
<button data-testid=”login-button”>
Login
</button>
You can write:
await page.getByTestId(‘login-button’).click();
By default, Playwright uses data-testid for getByTestId(), although the test ID attribute can be configured.
When should you use Test IDs?
Use a test ID when:
- User-facing text is unstable.
- The element has no useful accessible name.
- A component is difficult to identify semantically.
- Your development team has deliberately created a stable test contract.
Example:
<button data-testid=”checkout-submit”>
Continue
</button>
Test:
await page.getByTestId(‘checkout-submit’).click();
Important Rule
Do not add data-testid to every element automatically.
First ask:
Can I identify this element clearly using its role, label, text, or another stable user-facing property?
If yes, that may be preferable.
If not, a test ID can be a very good explicit contract.
8. Playwright locator()
page.locator() is the general locator API.
For example:
page.locator(‘#username’)
or:
page.locator(‘.login-button’)
You can also use CSS:
await page.locator(‘button[type=”submit”]’).click();
And XPath:
await page.locator(‘//button[@type=”submit”]’).click();
Playwright supports CSS and XPath, but official guidance recommends user-facing locators where practical.
CSS Selectors in Playwright
CSS selectors are widely used in web automation.
Examples:
page.locator(‘#username’)
page.locator(‘.login-button’)
page.locator(‘input[name=”email”]’)
page.locator(‘button[type=”submit”]’)
CSS can be useful when an element has a stable attribute that clearly identifies it.
Good CSS Example
page.locator(‘input[name=”email”]’)
Fragile CSS Example
page.locator(‘div.container > div:nth-child(2) > div > button’)
Why is the second example risky?
Because changing the page structure can break it.
If a developer adds one <div>, the selector may stop matching the intended element.
XPath in Playwright
XPath can locate elements based on attributes, text, relationships, and document structure.
Example:
await page.locator(‘//button[@type=”submit”]’).click();
Another example:
page.locator(‘//input[@name=”username”]’)
XPath is powerful, but powerful does not automatically mean better.
A long XPath tied to the DOM structure can become difficult to maintain.
Avoid This Style When Possible
page.locator(
‘/html/body/div[1]/div[2]/div[3]/form/div[2]/button’
)
If the page layout changes, this XPath can break.
Better Alternative
page.getByRole(‘button’, { name: ‘Login’ })
The best locator is not necessarily the shortest locator.
The best locator is the one that clearly identifies the intended element and remains useful when the UI changes.
Playwright Locator Priority: Which Locator Should You Choose?
There is no universal rule that says one locator must always be used for every element.
Instead, use this practical decision process.
Situation | Recommended Locator |
Button or link with clear name | getByRole() |
Form field with label | getByLabel() |
Visible message or text | getByText() |
Input with useful placeholder | getByPlaceholder() |
Image with meaningful alt text | getByAltText() |
Element with stable title | getByTitle() |
Explicit automation contract | getByTestId() |
Stable CSS attribute | locator() |
Complex fallback | CSS/XPath with locator() |
The official Playwright recommendation is to prioritize user-facing locators and explicit contracts.
The Most Important Playwright Locator Rule
Locate Elements the Way Users Understand Them
Compare:
page.locator(‘#loginButton’)
with:
page.getByRole(‘button’, { name: ‘Login’ })
The second version explains what the test actually cares about:
A user sees a Login button.
This is more meaningful than depending only on an implementation-specific ID.
However, this does not mean IDs are always bad.
If an ID is deliberately stable and meaningful, it can be perfectly valid.
The real goal is:
Choose a stable, readable and meaningful locator.
Locator Chaining in Playwright
Sometimes a page contains many elements with the same name.
Imagine an online shopping page:
Laptop
Add to cart
Keyboard
Add to cart
Mouse
Add to cart
This locator may match multiple buttons:
page.getByRole(‘button’, { name: ‘Add to cart’ })
Instead, first locate the product.
const keyboard = page
.getByRole(‘listitem’)
.filter({ hasText: ‘Keyboard’ });
Then find the button inside it:
await keyboard
.getByRole(‘button’, { name: ‘Add to cart’ })
.click();
This is locator chaining.
Playwright officially supports chaining and filtering to narrow locators to the required part of the page.
Playwright Locator Filtering
Filtering is extremely useful when a page contains repeated components.
Filter by Text
const product = page
.getByRole(‘listitem’)
.filter({ hasText: ‘Keyboard’ });
Then:
await product
.getByRole(‘button’, { name: ‘Add to cart’ })
.click();
Filter Using Another Locator
You can also use has.
const product = page
.getByRole(‘listitem’)
.filter({
has: page.getByRole(‘heading’, { name: ‘Keyboard’ })
});
This is useful when text alone is not enough to uniquely identify a component.
hasText vs has
These two concepts are easy to confuse.
hasText
Use it when you want to narrow an element based on text.
.filter({ hasText: ‘Keyboard’ })
has
Use it when you want to narrow an element based on another locator.
.filter({
has: page.getByRole(‘heading’, { name: ‘Keyboard’ })
})
A simple way to remember:
hasText → Find by text inside
has → Find by another locator inside
Playwright Strict Mode
One of the most important concepts beginners should understand is strictness.
Suppose your page contains three buttons named:
Save
Save
Save
You write:
await page.getByRole(‘button’, { name: ‘Save’ }).click();
Playwright may report a strict-mode violation because the locator identifies multiple matching elements while the action expects one specific target.
This is useful.
It tells you:
“Your locator is not specific enough.”
Instead of immediately using:
.first()
or:
.nth(1)
ask:
Why are multiple elements matching?
Maybe you should scope the locator.
For example:
const settings = page.getByRole(‘dialog’, { name: ‘Settings’ });
await settings
.getByRole(‘button’, { name: ‘Save’ })
.click();
Now the Save button is identified inside the Settings dialog.
This is usually more meaningful than blindly selecting the first matching button.
first(), last() and nth()
Playwright provides methods such as:
.first()
.last()
.nth(2)
Example:
await page.getByRole(‘button’).first().click();
Or:
await page.getByRole(‘button’).nth(2).click();
These can be useful when the order is genuinely meaningful.
But avoid using them simply to hide an ambiguous locator.
Risky Example
page.getByRole(‘button’, { name: ‘Delete’ }).first()
If developers reorder the buttons, your test could click a different Delete button.
Better Approach
Scope the locator to the correct component.
const userRow = page
.getByRole(‘row’)
.filter({ hasText: ‘John’ });
await userRow
.getByRole(‘button’, { name: ‘Delete’ })
.click();
Dynamic Elements and Playwright Locators
Modern websites often generate dynamic content.
Examples include:
- Product lists.
- Notifications.
- Search results.
- User tables.
- Dashboards.
- Popups.
- React components.
- Angular components.
- Infinite scrolling lists.
Avoid depending on unstable generated values when possible.
For example, this can be fragile:
page.locator(‘#user-483927’)
if the number changes every time.
Instead, identify the user using stable information:
page
.getByRole(‘row’)
.filter({ hasText: ‘John’ });
Then locate the required action within that row.
This is one of the biggest differences between writing a locator that works today and engineering a locator that survives tomorrow.
Playwright Locators and Auto-Waiting
One reason Playwright locators are powerful is that Playwright performs actionability checks before actions such as clicking.
For example:
await page.getByRole(‘button’, { name: ‘Submit’ }).click();
Playwright can wait for the target to become actionable instead of requiring you to add arbitrary sleeps.
The official best-practices documentation specifically recommends relying on Playwright’s locator auto-waiting rather than introducing unnecessary manual waiting.
Avoid This
await page.waitForTimeout(3000);
await page.getByRole(‘button’, { name: ‘Submit’ }).click();
The three-second delay may be:
- Too short on a slow system.
- Too long on a fast system.
- Unnecessary if Playwright can wait for the actionability condition.
Prefer:
await page.getByRole(‘button’, { name: ‘Submit’ }).click();
Locator vs Selector: What Is the Difference?
These terms are often used interchangeably, but there is a useful distinction.
A selector is a pattern used to identify an element.
Examples:
#username
.login-button
//button[@type=’submit’]
A locator is Playwright’s higher-level mechanism for identifying and interacting with elements.
Examples:
page.getByRole(‘button’, { name: ‘Login’ })
page.getByLabel(‘Username’)
page.locator(‘#username’)
So:
Selector = way to describe an element
Locator = Playwright object/API used to work with that element
Playwright Locators vs Selenium Locators
If you are coming from Selenium, the concept will feel familiar.
Selenium commonly uses:
id
name
class name
CSS
XPath
link text
Playwright supports CSS and XPath too, but it adds a strong set of user-facing locator APIs:
getByRole()
getByLabel()
getByText()
getByPlaceholder()
getByAltText()
getByTitle()
getByTestId()
Playwright’s locator model is designed around readable element identification, auto-waiting and retryability.
For someone moving from Selenium to Playwright, learning getByRole() and locator chaining is especially valuable.
Real-World Login Test Using Playwright Locators
Here is a complete beginner-friendly example.
import { test, expect } from ‘@playwright/test’;
test(‘login test’, async ({ page }) => {
await page.goto(‘https://example.com/login’);
await page.getByLabel(‘Username’).fill(‘john123’);
await page.getByLabel(‘Password’).fill(‘password123’);
await page.getByRole(‘button’, { name: ‘Login’ }).click();
await expect(
page.getByRole(‘heading’, { name: ‘Dashboard’ })
).toBeVisible();
});
Notice the structure:
Open page
↓
Find username
↓
Enter username
↓
Find password
↓
Enter password
↓
Find Login button
↓
Click
↓
Find Dashboard heading
↓
Verify
This is much easier to understand than a test filled with long XPath expressions.
Real-World E-Commerce Locator Example
Imagine an online store has:
Laptop
₹50,000
Add to cart
Keyboard
₹2,000
Add to cart
You want to add only the keyboard.
Use:
const keyboard = page
.getByRole(‘listitem’)
.filter({ hasText: ‘Keyboard’ });
await keyboard
.getByRole(‘button’, { name: ‘Add to cart’ })
.click();
The test is saying:
Find the product containing “Keyboard”, then click its Add to cart button.
That is a strong locator strategy because it describes the business action rather than the page’s exact HTML structure.
Locating Elements Inside a Dialog
Suppose a page contains multiple Save buttons but one belongs to a Settings dialog.
Use:
const settingsDialog = page.getByRole(‘dialog’, {
name: ‘Settings’
});
await settingsDialog
.getByRole(‘button’, { name: ‘Save’ })
.click();
This is an example of scoping.
Instead of searching the whole page, you tell Playwright where to search.
Locating Elements Inside an Iframe
Frames have their own document context.
Playwright provides frameLocator() for working with elements inside frames.
Example:
const frame = page.frameLocator(‘#payment-frame’);
await frame
.getByLabel(‘Card Number’)
.fill(‘4111111111111111’);
You can then continue locating elements inside that frame.
This is much cleaner than trying to treat iframe content as if it were part of the main document.
Using Regular Expressions with Locators
Regular expressions are useful when text changes slightly.
For example:
await expect(
page.getByText(/welcome, john/i)
).toBeVisible();
You can also use a regular expression with role names:
await page.getByRole(‘button’, {
name: /submit/i
}).click();
This can match variations such as:
Submit
SUBMIT
Submit Form
Use regular expressions when flexibility is useful.
Do not use them simply because exact matching is available.
Exact Matching in Playwright
Suppose the page contains:
Save
Save changes
Save and continue
This:
page.getByText(‘Save’)
may match more than you expect depending on the surrounding content.
Use:
page.getByText(‘Save’, { exact: true })
when you specifically need the exact text.
Exact matching is particularly useful when similar labels appear on the same page.
Locator Reuse
You can store a locator in a variable.
const loginButton = page.getByRole(
‘button’,
{ name: ‘Login’ }
);
await expect(loginButton).toBeVisible();
await loginButton.click();
This improves readability when the same locator is used multiple times.
It also works well with Page Object Model.
Playwright Locators in Page Object Model
In a real automation framework, you should avoid putting every locator directly inside every test.
A Page Object can store commonly used locators.
Example:
import { Page, Locator } from ‘@playwright/test’;
export class LoginPage {
readonly username: Locator;
readonly password: Locator;
readonly loginButton: Locator;
constructor(private page: Page) {
this.username = page.getByLabel(‘Username’);
this.password = page.getByLabel(‘Password’);
this.loginButton = page.getByRole(
‘button’,
{ name: ‘Login’ }
);
}
async login(username: string, password: string) {
await this.username.fill(username);
await this.password.fill(password);
await this.loginButton.click();
}
}
Your test becomes:
const loginPage = new LoginPage(page);
await loginPage.login(
‘john123’,
‘password123’
);
This keeps locator definitions in one place.
If the UI changes, you have fewer places to update.
Playwright Codegen for Finding Locators
You do not always need to write every locator from zero.
Playwright provides Codegen, which can generate test code while you interact with a webpage.
For example:
npx playwright codegen
Or:
npx playwright codegen https://example.com
Codegen can help beginners discover possible locators quickly.
But there is an important rule:
Do not blindly copy every locator generated by Codegen.
Review the generated locator.
Ask:
- Is it readable?
- Is it unique?
- Is it stable?
- Does it describe the user’s action?
- Could the text change frequently?
- Would a test ID be a better explicit contract?
- Is the locator unnecessarily complicated?
Playwright’s documentation recommends using Codegen to generate a locator and then editing it when appropriate.
Playwright Inspector for Locator Debugging
When a locator does not work, Playwright Inspector can help.
You can run:
npx playwright test –debug
The Inspector lets you pause execution, inspect elements and experiment with locators.
This is useful when:
- A locator matches multiple elements.
- A locator finds nothing.
- A dynamic component changes.
- A locator works in one state but not another.
- You are unsure about the accessible name.
A good debugging workflow is:
Failing locator
↓
Inspect the page
↓
Check matching elements
↓
Understand why it matches
↓
Narrow the locator
↓
Run the test again
Common Playwright Locator Errors
Error 1: Locator Matches Multiple Elements
Problem:
page.getByRole(‘button’, { name: ‘Save’ })
matches several buttons.
Solution:
Scope it.
const dialog = page.getByRole(‘dialog’, {
name: ‘Profile’
});
await dialog
.getByRole(‘button’, { name: ‘Save’ })
.click();
Error 2: Locator Finds No Element
Possible reasons include:
- Wrong accessible name.
- Element is inside an iframe.
- Element appears only after another action.
- Wrong page or URL.
- Incorrect selector.
- Element is rendered conditionally.
- Text does not match what you expected.
Debug the page state instead of immediately adding a timeout.
Common Locator Mistakes Beginners Make
Mistake 1: Using XPath for Everything
Example:
page.locator(‘//div[2]/div[1]/button’)
This can be difficult to maintain.
Try a user-facing locator first.
Mistake 2: Using nth() Too Quickly
This:
page.getByRole(‘button’).nth(4)
does not explain why the fifth button is the correct one.
Prefer meaningful scoping when possible.
Mistake 3: Using Arbitrary Waits
Avoid:
await page.waitForTimeout(5000);
when Playwright can synchronize with the element naturally.
Mistake 4: Selecting by CSS Classes That Are Used for Styling
Example:
page.locator(‘.blue-button’)
A designer can change the class without changing the application’s behavior.
Mistake 5: Creating Extremely Long Selectors
Long selectors often indicate that the locator strategy needs improvement.
Instead of:
page.locator(
‘div.container > div:nth-child(2) > form > div:nth-child(3) > button’
)
look for:
page.getByRole(‘button’, { name: ‘Submit’ })
Best Practices for Playwright Locators
1. Prefer User-Facing Locators
Start with:
getByRole()
getByLabel()
getByText()
when they clearly describe the intended element.
2. Use Accessible Names
Instead of:
getByRole(‘button’)
prefer:
getByRole(‘button’, { name: ‘Login’ })
when a meaningful accessible name exists.
This makes the locator more specific.
3. Scope Repeated Components
Instead of searching the entire page:
page.getByRole(‘button’, { name: ‘Delete’ })
scope it to the relevant row:
const row = page
.getByRole(‘row’)
.filter({ hasText: ‘John’ });
await row
.getByRole(‘button’, { name: ‘Delete’ })
.click();
4. Use Test IDs Intentionally
Use:
getByTestId()
when your team has deliberately created a stable test contract.
Do not add test IDs everywhere without a reason.
5. Avoid DOM-Position-Based Locators
Avoid relying on:
nth-child
first child
second child
deep XPath
unless the position itself is part of the requirement.
6. Let Playwright Handle Synchronization
Prefer Playwright’s built-in waiting behavior over arbitrary sleeps.
7. Treat Strictness as Feedback
If Playwright tells you that your locator matches multiple elements, do not automatically suppress the problem.
Ask:
“How can I make this locator describe the exact element I mean?”
8. Review Generated Locators
Codegen is a productivity tool.
It is not a replacement for locator design.
A Simple Playwright Locator Decision Tree
When you need to locate an element, ask these questions:
Is it an interactive element?
|
Yes
|
Does it have a clear role/name?
|
Yes
↓
getByRole()
|
No
↓
Is it a form field with a label?
|
Yes
↓
getByLabel()
|
No
↓
Does meaningful visible text identify it?
|
Yes
↓
getByText()
|
No
↓
Is there a useful placeholder?
|
Yes
↓
getByPlaceholder()
|
No
↓
Is there a stable test contract?
|
Yes
↓
getByTestId()
|
No
↓
Use a stable CSS/XPath locator
This decision tree is more useful than memorizing a single “best locator.”
Playwright Locators Cheat Sheet
Requirement | Example |
Button | page.getByRole(‘button’, { name: ‘Login’ }) |
Link | page.getByRole(‘link’, { name: ‘Home’ }) |
Heading | page.getByRole(‘heading’, { name: ‘Dashboard’ }) |
Checkbox | page.getByRole(‘checkbox’, { name: ‘Subscribe’ }) |
Radio | page.getByRole(‘radio’, { name: ‘Male’ }) |
Form field | page.getByLabel(‘Email’) |
Visible text | page.getByText(‘Success’) |
Placeholder | page.getByPlaceholder(‘Search’) |
Image | page.getByAltText(‘Logo’) |
Title | page.getByTitle(‘Settings’) |
Test ID | page.getByTestId(‘submit-button’) |
CSS | page.locator(‘#username’) |
XPath | page.locator(‘//button[@type=”submit”]’) |
Filter text | .filter({ hasText: ‘Laptop’ }) |
Filter locator | .filter({ has: locator }) |
First match | .first() |
Last match | .last() |
Specific index | .nth(2) |
Frame | page.frameLocator(‘#frame’) |
Playwright Locators: Beginner Practice Exercise
If you are learning Playwright, create a simple login page containing:
Username
Password
Login
Forgot Password?
Your task is to create locators for each element.
Username
page.getByLabel(‘Username’)
Password
page.getByLabel(‘Password’)
Login
page.getByRole(‘button’, { name: ‘Login’ })
Forgot Password
page.getByRole(‘link’, {
name: ‘Forgot Password?’
})
Then write a test that:
- Opens the login page.
- Enters the username.
- Enters the password.
- Clicks Login.
- Verifies the Dashboard.
This small exercise teaches the core locator skills needed for larger automation frameworks.
Intermediate Practice Exercise
Create an e-commerce page with five products.
Each product should contain:
Product Name
Price
Add to Cart
Write a test that adds only:
Wireless Keyboard
to the cart.
Do not use:
nth()
Instead, use:
filter()
and locator chaining.
This teaches you a skill that is used frequently in real automation projects.
Advanced Practice Exercise
Create a page containing:
- Multiple tables.
- Multiple rows.
- Multiple Edit buttons.
- Multiple Delete buttons.
- A Settings dialog.
- Dynamic notifications.
Then write tests that:
- Find a specific user.
- Edit only that user.
- Delete only that user.
- Verify a success notification.
- Work correctly even if other rows are added.
This exercise teaches scoping, filtering, strictness and stable locator design.
How to Make Playwright Locators Stable
A stable locator should answer three questions:
1. What am I locating?
Example:
Login button
2. Why is this the correct element?
Example:
It is the Login button inside the Login dialog.
3. What could change without breaking my locator?
If your locator depends on:
div number
CSS position
generated class
DOM depth
it may be fragile.
If it depends on:
role
accessible name
label
stable test ID
meaningful text
it may better represent the intended behavior.
Locator Stability Is More Important Than Locator Length
A common beginner mistake is thinking:
“The shorter locator is always better.”
That is not true.
Consider:
page.locator(‘button’)
It is very short.
But if the page has 15 buttons, it is not useful for identifying one specific button.
Now consider:
page.getByRole(‘button’, {
name: ‘Place Order’
})
It is slightly longer, but it communicates the exact purpose.
The goal is not:
Shortest locator.
The goal is:
Stable + readable + unique + meaningful locator.
Accessibility and Playwright Locators
There is another important benefit to role-based locators.
When you write:
page.getByRole(‘button’, {
name: ‘Submit’
})
you are testing the element in a way related to how users and assistive technologies perceive it.
This can encourage better accessible markup.
However, an important distinction is:
Using role locators does not mean your application has passed an accessibility audit.
Functional locator success is not the same as full accessibility compliance.
Playwright’s official documentation makes this distinction clear.
Playwright Locators for Dynamic Web Applications
Modern applications can change while a test is running.
For example:
User clicks Search
↓
API request starts
↓
Loading appears
↓
Results are rendered
↓
Buttons become available
A locator such as:
const result = page.getByRole(
‘button’,
{ name: ‘Buy Now’ }
);
can be used when the element becomes available.
You generally do not need to locate the element once and manually manage a stale DOM reference.
This locator-based model is one reason Playwright is well suited to modern web applications.
10 Rules for Writing Better Playwright Locators
Remember these rules:
Rule 1
Prefer meaningful user-facing locators.
Rule 2
Use getByRole() for clearly identifiable interactive elements.
Rule 3
Use getByLabel() for properly labelled form fields.
Rule 4
Use getByText() when visible text is the meaningful identifier.
Rule 5
Use getByTestId() when an explicit testing contract is appropriate.
Rule 6
Use chaining and filtering for repeated components.
Rule 7
Do not use nth() merely to hide ambiguous locators.
Rule 8
Avoid long DOM-dependent CSS and XPath when a stronger locator exists.
Rule 9
Do not add arbitrary waitForTimeout() calls to solve locator problems.
Rule 10
When a locator fails, understand why before changing it.
Final Takeaway
Playwright Locators are the foundation of reliable Playwright automation.
Do not learn them as a list of commands.
Learn how to choose the right locator for the right situation.
Start with:
getByRole()
getByLabel()
getByText()
getByPlaceholder()
getByAltText()
getByTitle()
getByTestId()
Then learn:
locator()
CSS
XPath
chaining
filter()
hasText
has
strictness
frames
dynamic elements
Codegen
Inspector
Most importantly, remember this:
A good locator does not merely find an element. It explains why that element is the one your test intends to use.
If you can look at a real application and quickly decide whether to use getByRole(), getByLabel(), getByText(), getByTestId(), filtering, chaining, CSS, or XPath, you have moved beyond memorizing Playwright syntax.
You are developing a real Playwright automation engineering skill.
Frequently Asked Questions About Playwright Locators
What are Playwright Locators?
Playwright locators are APIs used to identify elements on a webpage so Playwright can interact with or verify them.
Examples include getByRole(), getByText(), getByLabel(), getByPlaceholder(), getByTestId(), and locator().
Which Playwright locator is best?
There is no single locator that is best for every element.
For many interactive elements, start with:
getByRole()
For form fields, consider:
getByLabel()
For meaningful visible text:
getByText()
For an explicit testing contract:
getByTestId()
Use CSS or XPath when they provide a suitable stable way to identify an element.
Playwright’s official guidance is to prioritize user-facing locators and explicit contracts.
Is getByRole() better than CSS?
For many user-facing interactive elements, getByRole() is a strong choice because it describes the element semantically.
For example:
page.getByRole(‘button’, { name: ‘Login’ })
can communicate intent more clearly than:
page.locator(‘#login’)
However, a stable CSS selector can still be appropriate when it represents a reliable application or testing contract.
Is XPath supported in Playwright?
Yes.
For example:
page.locator(‘//button[@type=”submit”]’)
Playwright supports XPath through locator(). However, long structural XPath expressions can be difficult to maintain, so use them when they are genuinely useful rather than as the default strategy.
Is CSS supported in Playwright?
Yes.
Example:
page.locator(‘#username’)
CSS is useful when you have a stable and meaningful selector.
Avoid building selectors that depend heavily on DOM position or presentation-only classes.
What is getByTestId() in Playwright?
getByTestId() finds an element using a test ID.
Example:
page.getByTestId(‘login-button’)
The default attribute is:
data-testid
Example:
<button data-testid=”login-button”>
Login
</button>
Playwright also allows the test ID attribute to be configured.
What is locator chaining in Playwright?
Locator chaining means using one locator to narrow the search before locating another element.
Example:
const product = page
.getByRole(‘listitem’)
.filter({ hasText: ‘Laptop’ });
await product
.getByRole(‘button’, { name: ‘Add to cart’ })
.click();
It is especially useful for repeated UI components.
What is Playwright locator filtering?
Filtering narrows a locator to specific elements.
Example:
page
.getByRole(‘listitem’)
.filter({ hasText: ‘Laptop’ });
You can also filter using another locator:
.filter({
has: page.getByRole(‘heading’, { name: ‘Laptop’ })
})
What is Playwright strict mode?
Playwright’s strictness helps identify cases where a locator intended for a single element actually matches multiple elements.
Instead of hiding the ambiguity with .first() or .nth(), improve the locator so it clearly identifies the intended element.
Should I use XPath for every Playwright element?
No.
XPath is powerful, but it should not automatically be your first choice.
Try:
getByRole()
getByLabel()
getByText()
getByPlaceholder()
getByTestId()
before writing complicated XPath expressions.
Should I use CSS selectors or Playwright locators?
Use whichever provides the clearest stable locator for the element.
For example:
page.getByRole(‘button’, { name: ‘Login’ })
is often a strong choice for a Login button.
A CSS selector such as:
page.locator(‘[data-testid=”login-button”]’)
may be appropriate when your application deliberately defines that test contract.
How do I debug a Playwright locator?
Start with:
npx playwright test –debug
You can use Playwright Inspector to inspect the page, test locators and understand why a locator matches or fails.
Also check whether the element is inside an iframe, whether its accessible name is what you expect, and whether multiple elements match.
Why does my Playwright locator work sometimes and fail sometimes?
Common causes include:
- Unstable locator.
- Dynamic page content.
- Multiple matching elements.
- Incorrect page state.
- Iframe content.
- Changing text.
- Race conditions caused by poor synchronization.
- Overly specific CSS or XPath.
First inspect the locator and page state instead of adding a fixed sleep.
Do Playwright locators automatically wait?
Playwright locators work with Playwright’s auto-waiting and retryability mechanisms. Before actions, Playwright performs relevant actionability checks rather than simply clicking immediately.
This is one reason Playwright tests can be written without adding arbitrary delays for many normal UI interactions.
Can Playwright locators handle React applications?
Yes.
Playwright locators are designed to work with dynamic modern web applications, including applications whose DOM is updated or re-rendered.
A locator is resolved against the current page state when it is used, which helps with changing DOM content.
Can I use Playwright locators with Page Object Model?
Yes.
Locators are commonly stored as properties in Page Object classes.
Example:
class LoginPage {
username = this.page.getByLabel(‘Username’);
password = this.page.getByLabel(‘Password’);
login = this.page.getByRole(‘button’, {
name: ‘Login’
});
}
This centralizes locator definitions and makes large test suites easier to maintain.
Playwright Masters Team
Playwright Automation Testing Experts | Industry-Focused Training & Practical Learning
Playwright Masters is a dedicated automation testing training platform focused on helping learners build practical skills in Playwright automation testing. Our training covers real-world automation concepts, framework practices, coding, debugging, API testing, cross-browser testing, CI/CD, and interview preparation to help learners develop job-ready testing skills.