← Back to Index

Playwright Locators

Learn different types of Playwright locators, how Playwright finds elements, why some locators are more robust than others, and how to design locator strategies that survive UI changes.

Beginner → Practical → Robust Automation → Professional

1. What is a Locator?

A locator is Playwright's way of identifying an element on a web page. Instead of directly depending on an element's current position in the DOM, Playwright allows you to describe the element using user-facing properties, accessibility information, CSS, text, labels, roles, test IDs, and more.

Test Code
→
Locator
→
Find Element
→
Auto-Wait
→
Action
Important: Playwright locators are not simply "element selectors." They provide a higher-level mechanism that can locate an element and automatically wait for it to become actionable before performing actions.

2. Different Types of Playwright Locators

1

Role Locator

Locates elements using their accessible role and accessible name.

Recommended
2

Label Locator

Useful for form fields associated with labels.

Recommended
3

Placeholder Locator

Finds inputs using their placeholder text.

Useful
4

Text Locator

Locates elements based on visible text.

Useful
5

Test ID

Uses a dedicated testing attribute such as data-testid.

Very Stable
6

CSS Locator

Uses CSS selectors to locate elements.

Use Carefully
7

XPath

Uses XPath expressions to locate elements.

Last Resort
8

Chained Locator

Narrows the search from a parent element to a child element.

Professional
9

Filtering

Finds elements using text, attributes, or other locator conditions.

Professional

3. Role Locator — Preferred Starting Point

Playwright recommends user-facing locators such as roles because they closely represent how users and assistive technologies interact with the application.

HTML

HTML
<button>Login</button>

<button>Register</button>

<input type="email" aria-label="Email">

Playwright

Python
page.get_by_role("button", name="Login").click()

page.get_by_role("button", name="Register").click()

page.get_by_role("textbox", name="Email").fill("user@example.com")
Why this is robust: If the button's CSS class changes from btn-primary to button-main, the role-based locator can continue to work as long as the user-facing semantics remain the same.

4. Label Locator — Excellent for Forms

When an input is associated with a visible label, use get_by_label().

HTML
<label for="email">Email Address</label>
<input id="email" type="email">

<label for="password">Password</label>
<input id="password" type="password">
Python
page.get_by_label("Email Address").fill("user@example.com")

page.get_by_label("Password").fill("Secret123")
Real-world benefit: The test describes what the user sees rather than depending on an implementation-specific CSS class.

5. Placeholder Locator

Inputs frequently contain placeholder text such as "Search products", "Enter email", or "Search customers".

HTML
<input
    type="text"
    placeholder="Search products"
>
Python
page.get_by_placeholder("Search products").fill("Laptop")
Be careful: Placeholder text can change because of UX or product-copy changes. If the application has a stable accessible label, prefer get_by_label().

6. Text Locator

Text locators are useful when a user-visible piece of text uniquely identifies an element.

Python
page.get_by_text("Order placed successfully").click()

Example

Python
message = page.get_by_text("Payment successful")

message.wait_for()
Text is useful for user-visible content, but avoid depending on long, frequently changing marketing copy when a more stable semantic locator exists.

7. Test ID — Strong Contract Between App and Test

A dedicated test ID can be one of the most stable ways to identify an element when user-facing semantics are not unique or appropriate.

Application HTML

HTML
<button data-testid="checkout-button">
    Checkout
</button>

Playwright

Python
page.get_by_test_id("checkout-button").click()
Professional approach: Establish a small, intentional set of stable test IDs for important application components instead of adding test IDs to every DOM element.

8. CSS Locator

CSS selectors are powerful and sometimes necessary, especially when working with custom components or attributes.

Python
page.locator("#login-button").click()

page.locator(".product-card").first.click()

page.locator("[data-status='active']").click()

Attribute Selector

Python
page.locator("input[name='email']").fill("user@example.com")
Do not automatically choose CSS just because DevTools generated it. Long selectors based on DOM structure or generated classes are often fragile.

9. XPath Locator

Playwright supports XPath, but XPath should generally not be your first choice when a more resilient locator is available.

Python
page.locator("//button[@type='submit']").click()

Fragile XPath Example

Python
page.locator(
    "/html/body/div[2]/div[1]/div/form/div[3]/button"
).click()
Why this is fragile: A small DOM restructuring can break the selector even though the application behavior has not changed.

10. Chained Locators — Find the Right Element Inside a Component

Modern applications frequently contain repeated components. Instead of selecting a generic button globally, first identify the correct component and then locate the button inside it.

Example UI

HTML
<div class="product-card">
    <h3>Laptop</h3>
    <button>Add to Cart</button>
</div>

<div class="product-card">
    <h3>Phone</h3>
    <button>Add to Cart</button>
</div>

Robust Locator

Python
laptop = page.get_by_role(
    "heading",
    name="Laptop"
).locator("..")

laptop.get_by_role(
    "button",
    name="Add to Cart"
).click()

An even stronger approach for component-heavy applications is to give the component a stable test ID and locate the child element from it.

Python
product = page.get_by_test_id("product-card-laptop")

product.get_by_role(
    "button",
    name="Add to Cart"
).click()

11. Filtering Locators

Filtering is extremely useful when a page contains many similar elements.

Example

Python
product = page.locator(".product-card").filter(
    has_text="Laptop"
)

product.get_by_role(
    "button",
    name="Add to Cart"
).click()

Filtering by Child Locator

Python
card = page.locator(".product-card").filter(
    has=page.get_by_role(
        "heading",
        name="Laptop"
    )
)

card.get_by_role(
    "button",
    name="Add to Cart"
).click()
This approach is especially valuable when testing tables, product listings, customer cards, order rows, dashboards, and repeated UI components.

12. Nested Locators for Real Applications

Consider an application showing customer orders. Each order has a customer name, status, amount and action buttons.

Python
order = page.locator(".order-row").filter(
    has_text="John Smith"
)

order.get_by_role(
    "button",
    name="View Order"
).click()

The test is not dependent on whether John's order happens to be row 1, row 5, or row 100.

Think semantically: First identify the business object. Then identify the action inside that object.

13. How Playwright Makes Locators Robust

One of Playwright's important strengths is that locator actions automatically wait for the target element to become ready.

Locator
→
Element Found
→
Visible?
→
Enabled?
→
Stable?
→
Click
Python
page.get_by_role(
    "button",
    name="Submit"
).click()

You generally do not need to write a manual sleep before every action. Playwright waits for relevant actionability conditions and retries locating the element when appropriate.

Avoid using: time.sleep(5) as your default synchronization strategy.

14. Dynamic Element Example

Imagine a shopping application where the Checkout button appears only after the cart has finished loading.

Python
page.get_by_test_id("cart-loader").wait_for(
    state="hidden"
)

page.get_by_role(
    "button",
    name="Checkout"
).click()

For assertions, prefer Playwright's web-first assertions.

Python
from playwright.sync_api import expect

expect(
    page.get_by_role(
        "button",
        name="Checkout"
    )
).to_be_visible()

15. Locator Comparison

Locator Example Typical Stability Best Use
Role get_by_role() High Buttons, links, headings, controls
Label get_by_label() High Forms and inputs
Test ID get_by_test_id() Very High Stable application contracts
Placeholder get_by_placeholder() Medium Inputs without strong labels
Text get_by_text() Medium User-visible messages and content
CSS locator(".class") Variable Custom DOM targeting
XPath locator("//...") Variable Special cases
Filter locator().filter() High Repeated components

16. Fragile vs Robust Locators

❌ Fragile

Avoid
page.locator(
    "div.container:nth-child(2) > div:nth-child(3) > button"
).click()

❌ Generated Class

Avoid When Possible
page.locator(".css-1x7abc9").click()

❌ Index-Based Selection

Avoid
page.locator("button").nth(4).click()

✅ Robust

Recommended
page.get_by_role(
    "button",
    name="Submit Order"
).click()

17. Real-Time Example — Login Page

Consider a typical enterprise login page.

HTML
<label for="username">Username</label>
<input id="username" type="text">

<label for="password">Password</label>
<input id="password" type="password">

<button type="submit">Sign In</button>
Python
def login(page, username, password):

    page.get_by_label("Username").fill(username)

    page.get_by_label("Password").fill(password)

    page.get_by_role(
        "button",
        name="Sign In"
    ).click()
The test reads almost like the business action: enter username → enter password → sign in.

18. Real-Time Example — E-Commerce Product

Assume a page contains hundreds of products. Each product has the same "Add to Cart" button.

Python
product = page.locator(".product-card").filter(
    has_text="MacBook Pro"
)

product.get_by_role(
    "button",
    name="Add to Cart"
).click()

This is more maintainable than selecting the 7th "Add to Cart" button.

19. Real-Time Example — Customer Table

Imagine an admin dashboard containing thousands of customer records.

Python
customer = page.locator("tr").filter(
    has_text="John Smith"
)

customer.get_by_role(
    "button",
    name="View"
).click()

The locator identifies the row based on its content and then performs the action inside that specific row.

20. Locator Inside an iframe

Modern applications sometimes embed payment widgets, support tools, reports, or third-party components inside iframes.

Python
frame = page.frame_locator(
    "#payment-frame"
)

frame.get_by_label(
    "Card number"
).fill("4111111111111111")
Important: Elements inside an iframe belong to a different document. Use frame_locator() to work with them.

21. Debugging Locators

When a locator does not behave as expected, first inspect how many elements it matches.

Python
locator = page.get_by_role(
    "button",
    name="Submit"
)

print(locator.count())

Playwright's Inspector and code generation tools can also help you discover and validate locators during development.

22. Recommended Locator Strategy

Role
→
Label
→
Test ID
→
Text
→
Placeholder
→
CSS
→
XPath
This is a practical decision guide, not a strict rule. The correct locator depends on the application's accessibility semantics, stability, uniqueness, and testability.

23. Professional Locator Design

A professional automation framework should treat locators as part of the test architecture rather than scattering selectors throughout tests.

Example Page Object

Python
from playwright.sync_api import Page


class LoginPage:

    def __init__(self, page: Page):
        self.page = page

        self.username = page.get_by_label("Username")
        self.password = page.get_by_label("Password")

        self.sign_in = page.get_by_role(
            "button",
            name="Sign In"
        )

    def login(self, username, password):

        self.username.fill(username)

        self.password.fill(password)

        self.sign_in.click()

Test

Python
def test_login(page):

    login_page = LoginPage(page)

    login_page.login(
        "testuser",
        "TestPassword123"
    )
Now the test describes the business operation rather than the DOM implementation.

24. What Makes a Locator Robust?

  • It identifies the intended element uniquely.
  • It reflects stable user-facing behavior or an intentional test contract.
  • It does not depend unnecessarily on DOM position.
  • It does not depend on generated CSS classes.
  • It avoids unnecessary nth() usage.
  • It works correctly when the page contains repeated components.
  • It can survive reasonable UI implementation changes.
  • It works naturally with Playwright's auto-waiting behavior.
  • It is readable enough for another automation engineer to understand.

25. Bad vs Good Locator Thinking

Bad Question Better Question
What is the CSS class? What does this element represent to the user?
What is the element's position? What uniquely identifies this component?
Can I use XPath? Is there a semantic or stable locator available?
Can I click the 5th button? Which business component contains the button?
How do I force the click? Why is the intended element not actionable?

26. Complete Robust Locator Example

Python - Complete Example
from playwright.sync_api import Page, expect


def test_customer_order(page: Page):

    page.goto("https://example.com/orders")

    # Find the customer row using business-visible information
    customer = page.locator("tr").filter(
        has_text="John Smith"
    )

    # Verify customer is displayed
    expect(customer).to_be_visible()

    # Find the order action inside that customer row
    customer.get_by_role(
        "button",
        name="View Order"
    ).click()

    # Verify destination
    expect(
        page.get_by_role(
            "heading",
            name="Order Details"
        )
    ).to_be_visible()
This example combines several Playwright concepts: locator + filtering + semantic role + auto-waiting + web-first assertion.

27. Playwright Locator Cheat Sheet

Goal Playwright
Button page.get_by_role("button", name="Login")
Link page.get_by_role("link", name="Home")
Input by label page.get_by_label("Email")
Input by placeholder page.get_by_placeholder("Search")
Visible text page.get_by_text("Success")
Test ID page.get_by_test_id("submit")
CSS page.locator(".submit-button")
XPath page.locator("//button")
Filter by text locator.filter(has_text="Laptop")
Filter by locator locator.filter(has=child_locator)
Iframe page.frame_locator("#frame")

28. Locator Best Practices

  • Prefer get_by_role() when the element has meaningful accessible semantics.
  • Use get_by_label() for properly labeled form controls.
  • Use get_by_test_id() when a stable testing contract is appropriate.
  • Use text when the visible text itself is a meaningful identifier.
  • Use filtering for repeated components such as cards and table rows.
  • Avoid selectors that depend on generated CSS class names.
  • Avoid long DOM paths.
  • Avoid unnecessary nth() selectors.
  • Avoid fixed sleeps as a synchronization strategy.
  • Use Playwright's web-first assertions for dynamic UI validation.

29. Final Mental Model

Understand the UI
→
Identify the Element
→
Choose Stable Locator
→
Let Playwright Wait
→
Perform Action
→
Assert Result
Professional mindset: Don't ask, "How can I find this element?" Ask, "What stable piece of information uniquely identifies this element from a user's or application's perspective?"

Once you understand this mindset, locator design becomes much easier. Playwright then provides the synchronization and retry behavior around those locators so your tests can remain readable and maintainable.

30. Next Learning Step

Next → Assertions

Learn how Playwright verifies text, visibility, URL, attributes, values and application state using web-first assertions.

Then → Waits

Understand auto-waiting, actionability, explicit waiting, network synchronization and why hard sleeps should be avoided.

Professional → Page Object Model

Move locators into reusable page classes and build scalable automation frameworks.