← Back

Playwright XPath Locators

Learn how to use XPath with Playwright, build robust real-world locators, and understand why Playwright's locator model can make XPath-based automation easier to maintain than traditional Selenium XPath code.

1. What Is XPath?

XPath is a query language used to locate elements in an HTML or XML document. It allows automation engineers to find elements using relationships, attributes, text, hierarchy and position.

HTML DOM
→
XPath Expression
→
Matching Element
→
Playwright Action

Attribute Based

Locate an element using attributes such as @id, @name or @data-testid.

Text Based

Locate elements based on their visible text.

Relationship Based

Navigate from parent to child, sibling or another related element.

2. Basic XPath in Playwright

Playwright can use XPath through the standard locator() API.

Python - Basic XPath
page.locator("//button").click()

page.locator("//input[@id='email']").fill(
    "john@example.com"
)

page.locator("//input[@name='password']").fill(
    "Password123"
)
Important: page.locator() is the main Playwright locator API. XPath is one of the selector strategies that can be used with it.
3. Attribute-Based XPath

Attribute XPath is useful when the application provides stable attributes.

Common Attribute XPath
# ID
page.locator("//input[@id='username']").fill("john")

# Name
page.locator("//input[@name='email']").fill(
    "john@example.com"
)

# Class
page.locator("//button[@class='login-button']").click()

# Data attribute
page.locator("//button[@data-testid='login']").click()

# Multiple attributes
page.locator(
    "//input[@type='email' and @name='email']"
).fill("john@example.com")
4. Text-Based XPath

XPath can locate elements using their text content.

Text XPath
# Exact text
page.locator(
    "//button[text()='Login']"
).click()

# Text contained inside an element
page.locator(
    "//button[contains(text(),'Login')]"
).click()

# Link text
page.locator(
    "//a[text()='Orders']"
).click()

# Heading text
page.locator(
    "//h1[text()='Customer Dashboard']"
).is_visible()
Practical tip: For simple user-facing text, Playwright also provides get_by_text(). Use XPath when the relationship or structure you need is more expressive than a simple text locator.
5. Real-Time Example — Banking Login

Imagine a banking application with this HTML:

Example HTML
<div class="login-panel">

    <input id="customerId"
           name="customerId"
           type="text">

    <input id="password"
           name="password"
           type="password">

    <button class="login-btn">
        Login
    </button>

</div>

A Playwright test can use XPath like this:

Playwright Test
from playwright.sync_api import expect

def test_banking_login(page):

    page.goto("https://example.test/banking/login")

    page.locator(
        "//input[@id='customerId']"
    ).fill("CUST10001")

    page.locator(
        "//input[@id='password']"
    ).fill("Secret123")

    page.locator(
        "//button[contains(normalize-space(), 'Login')]"
    ).click()

    expect(page).to_have_url(
        r".*/dashboard"
    )
6. XPath Power — Element Relationships

One of XPath's biggest strengths is navigating relationships between elements.

<div class="customer-card">
<span>John Smith</span>
<span>john@example.com</span>
<button>Edit</button>

Suppose there are 50 customer cards and every card has an Edit button. We want to click Edit only for John Smith.

Relationship XPath
page.locator(
    "//div[contains(@class,'customer-card')]"
    "[.//span[normalize-space()='John Smith']]"
    "//button[normalize-space()='Edit']"
).click()
This is where XPath becomes useful: Instead of finding the first "Edit" button on the page, the XPath describes the relationship between the customer and the button.
7. Real-Time Example — E-Commerce Product Card

Imagine an e-commerce application containing many products.

Product HTML
<div class="product-card">

    <h3>Laptop Pro 15</h3>

    <span class="price">$1299</span>

    <button>Add to Cart</button>

</div>

We want to add only Laptop Pro 15 to the cart.

XPath Solution
page.locator(
    "//div[contains(@class,'product-card')]"
    "[.//h3[normalize-space()='Laptop Pro 15']]"
    "//button[normalize-space()='Add to Cart']"
).click()
8. Parent Axis

XPath can move from a known child element back to its parent.

Parent Relationship
# Find the input and move to its parent
page.locator(
    "//input[@name='email']/parent::*"
)

# Find customer name and move to customer card
page.locator(
    "//span[normalize-space()='John Smith']/ancestor::div[contains(@class,'customer-card')]"
)
Common XPath axes: parent::, ancestor::, child::, descendant::, following-sibling::, preceding-sibling::.
9. Sibling Relationships

XPath can locate an element based on another element next to it.

Form Example
<label>Email</label>
<input type="email">

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

Find the input following the Password label:

Following Sibling XPath
page.locator(
    "//label[normalize-space()='Password']"
    "/following-sibling::input[1]"
).fill("Password123")
10. Real-Time Example — Admin Order Table

Tables are a common place where relationship-based XPath is useful.

Order Table
<tr>
    <td>ORD-1001</td>
    <td>John Smith</td>
    <td>Paid</td>
    <td>
        <button>View</button>
    </td>
</tr>

Find the row for order ORD-1001 and click View.

Playwright XPath
order_row = page.locator(
    "//tr[.//td[normalize-space()='ORD-1001']]"
)

order_row.locator(
    ".//button[normalize-space()='View']"
).click()
11. Dynamic Attributes with contains()

Modern applications often generate dynamic IDs and classes.

Dynamic HTML
<button id="submit-btn-983472">
    Submit Order
</button>

<button id="submit-btn-239812">
    Submit Order
</button>

If the changing part is not important, XPath can target the stable portion.

contains() XPath
page.locator(
    "//button[contains(@id,'submit-btn-')]"
).click()
Be careful: contains() can match multiple elements. Prefer a stronger condition when possible.
12. normalize-space() for Messy Text

Real applications may contain extra whitespace around text.

HTML
<button>
    Save Customer
</button>
XPath
page.locator(
    "//button[normalize-space()='Save Customer']"
).click()
13. XPath Position and Index

XPath can select elements by position.

Position Example
# First matching button
page.locator(
    "(//button[contains(@class,'action')])[1]"
).click()

# Second matching button
page.locator(
    "(//button[contains(@class,'action')])[2]"
).click()
Professional rule: Position-based XPath should generally be a last resort. If the page changes order, the test may start interacting with the wrong element.
14. Playwright XPath vs Selenium XPath

Both Playwright and Selenium support XPath. The important difference is not that XPath itself becomes a different language. The difference is how XPath fits into the overall automation framework.

Capability Playwright Selenium
XPath support Yes Yes
CSS support Yes Yes
Locator abstraction Strong locator API through locator() Uses WebElement-oriented APIs and locator strategies
Auto-waiting / actionability Playwright actions wait for relevant actionability conditions Selenium provides explicit and implicit wait mechanisms; synchronization is commonly designed separately
Web-first assertions expect() supports retrying assertions Assertions are commonly provided through the test framework
Locator composition Supports filtering and chaining locators Supports locating elements, but the API model differs
User-facing locators Role, text, label and other locator APIs Commonly implemented with CSS, XPath, ID, name, link text and other strategies
15. Same Login — Selenium vs Playwright

Selenium

Selenium Python
from selenium.webdriver.common.by import By

driver.find_element(
    By.XPATH,
    "//input[@id='customerId']"
).send_keys("CUST10001")

driver.find_element(
    By.XPATH,
    "//input[@id='password']"
).send_keys("Secret123")

driver.find_element(
    By.XPATH,
    "//button[normalize-space()='Login']"
).click()

Playwright

Playwright Python
page.locator(
    "//input[@id='customerId']"
).fill("CUST10001")

page.locator(
    "//input[@id='password']"
).fill("Secret123")

page.locator(
    "//button[normalize-space()='Login']"
).click()
Notice: The XPath expression itself is essentially the same. Playwright's practical difference comes from its broader locator, actionability and assertion model rather than from XPath syntax alone.
16. Playwright Advantage — Locator Composition

Playwright lets you combine a broad locator with filtering and then perform another locator operation inside the matched region.

XPath Approach

XPath Relationship
page.locator(
    "//div[contains(@class,'product-card')]"
    "[.//h3[normalize-space()='Laptop Pro 15']]"
    "//button[normalize-space()='Add to Cart']"
).click()

Playwright Locator Composition

Locator Filtering
product = page.locator(
    ".product-card"
).filter(
    has_text="Laptop Pro 15"
)

product.get_by_role(
    "button",
    name="Add to Cart"
).click()
Why this can be easier to maintain: The test can express the business relationship in smaller locator steps instead of putting the entire relationship into one large XPath expression.
17. XPath vs Playwright Role Locators

XPath is powerful, but it should not automatically be your first choice for every element.

Complex XPath

page.locator(
    "//div[@class='header']"
    "//nav//ul/li[2]"
    "//a[contains(text(),'Orders')]"
).click()

Semantic Locator

page.get_by_role(
    "link",
    name="Orders"
).click()
Rule of thumb: If the element has a clear user-facing role, label or accessible name, a Playwright semantic locator may be easier to understand. Use XPath when the DOM relationship itself is what you need.
18. Real-Time Example — Admin Customer Management

Imagine an admin page containing hundreds of customers. Each customer has Edit, Disable and Delete buttons.

Business Requirement
Find customer:
    John Smith

Then:
    Click Edit

Do NOT:
    Click another customer's Edit button

XPath Solution

Playwright + XPath
customer = page.locator(
    "//div[contains(@class,'customer-card')]"
    "[.//*[normalize-space()='John Smith']]"
)

customer.locator(
    ".//button[normalize-space()='Edit']"
).click()

Locator Composition Alternative

Playwright Locator Model
customer = page.locator(
    ".customer-card"
).filter(
    has_text="John Smith"
)

customer.get_by_role(
    "button",
    name="Edit"
).click()
19. XPath + Web-First Assertions

A major Playwright advantage is that the locator can be used directly with web-first assertions.

Playwright Assertion
from playwright.sync_api import expect

success_message = page.locator(
    "//div[contains(@class,'success-message')]"
)

expect(success_message).to_be_visible()

You can also assert text:

Text Assertion
expect(
    page.locator(
        "//div[contains(@class,'success-message')]"
    )
).to_contain_text("Order created")
20. Avoid Hard Waits

XPath does not remove the need for synchronization. The automation framework still needs to wait for the application state you actually care about.

❌ Avoid

import time

page.locator(
    "//button[normalize-space()='Submit']"
).click()

time.sleep(5)

assert page.locator(
    "//div[@class='success']"
).is_visible()

✓ Preferred

from playwright.sync_api import expect

page.locator(
    "//button[normalize-space()='Submit']"
).click()

expect(
    page.locator(
        "//div[contains(@class,'success')]"
    )
).to_be_visible()
21. Real-Time Example — Order Processing

Consider an order management application.

Search Order
→
Open Order
→
Approve
→
Validate Status
Professional Example
from playwright.sync_api import expect

def test_approve_order(page):

    page.goto(
        "https://example.test/admin/orders"
    )

    order = page.locator(
        "//tr[.//td[normalize-space()='ORD-1001']]"
    )

    order.locator(
        ".//button[normalize-space()='Approve']"
    ).click()

    expect(
        order.locator(
            ".//td[normalize-space()='Approved']"
        )
    ).to_be_visible()
22. Useful XPath Functions
XPath Purpose Example
text() Match direct text //button[text()='Save']
contains() Partial match //button[contains(@id,'save')]
normalize-space() Normalize whitespace //button[normalize-space()='Save']
parent:: Move to parent //input/parent::div
ancestor:: Find an ancestor //span/ancestor::div
following-sibling:: Find following sibling //label/following-sibling::input
and Combine conditions //input[@type='email' and @required]
23. When XPath Is Especially Useful

Complex Relationships

When the target is identified through a parent, ancestor or sibling relationship.

Dynamic Attributes

When only part of an attribute is stable and contains() is appropriate.

Legacy Applications

Older applications may expose limited semantic or test-specific attributes.

Complex Tables

Find a row using one cell and interact with another cell or control inside that row.

Nested Components

XPath can express relationships through deeply nested DOM structures.

Migration Projects

Existing Selenium XPath expressions can often be moved into Playwright's locator API with relatively small changes.

24. When XPath Should NOT Be Your First Choice

XPath is powerful, but more power does not automatically mean a better locator.

❌ Unnecessarily Complex

page.locator(
    "//div[@class='page']"
    "//div[@class='header']"
    "//nav//ul/li[2]"
    "//a[contains(text(),'Orders')]"
).click()

✓ Prefer Semantic Locator

page.get_by_role(
    "link",
    name="Orders"
).click()

The second locator communicates the user's intent more clearly and is less tied to the page's DOM hierarchy.

25. Selenium XPath + Explicit Wait Comparison

In Selenium, a common pattern is to combine XPath with an explicit wait.

Selenium Python
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

login_button = WebDriverWait(
    driver,
    10
).until(
    EC.element_to_be_clickable(
        (
            By.XPATH,
            "//button[normalize-space()='Login']"
        )
    )
)

login_button.click()

Playwright

Playwright Python
page.locator(
    "//button[normalize-space()='Login']"
).click()
Important distinction: This does not mean Selenium cannot wait or that Playwright never needs synchronization. Both frameworks support synchronization. Playwright's action model handles relevant actionability checks automatically for many interactions, which can reduce explicit waiting code.
26. Complete Professional Example

Here is a realistic customer-order workflow using XPath, assertions and business-level steps.

Login
→
Find Customer
→
Open Order
→
Approve Order
→
Validate
Complete Playwright Test
from playwright.sync_api import expect

def test_customer_order_approval(page):

    # --------------------------------
    # 1. Login
    # --------------------------------

    page.goto(
        "https://example.test/admin/login"
    )

    page.locator(
        "//input[@name='email']"
    ).fill("admin@example.com")

    page.locator(
        "//input[@name='password']"
    ).fill("AdminPassword123")

    page.locator(
        "//button[normalize-space()='Sign In']"
    ).click()

    expect(page).to_have_url(
        r".*/dashboard"
    )

    # --------------------------------
    # 2. Open Customers
    # --------------------------------

    page.get_by_role(
        "link",
        name="Customers"
    ).click()

    # --------------------------------
    # 3. Find John Smith
    # --------------------------------

    customer = page.locator(
        "//div[contains(@class,'customer-card')]"
        "[.//*[normalize-space()='John Smith']]"
    )

    expect(customer).to_be_visible()

    # --------------------------------
    # 4. Open Orders
    # --------------------------------

    customer.locator(
        ".//a[normalize-space()='Orders']"
    ).click()

    # --------------------------------
    # 5. Find order
    # --------------------------------

    order = page.locator(
        "//tr[.//td[normalize-space()='ORD-1001']]"
    )

    expect(order).to_be_visible()

    # --------------------------------
    # 6. Approve
    # --------------------------------

    order.locator(
        ".//button[normalize-space()='Approve']"
    ).click()

    # --------------------------------
    # 7. Validate
    # --------------------------------

    expect(
        order.locator(
            ".//td[normalize-space()='Approved']"
        )
    ).to_be_visible()
27. Professional Locator Strategy

A professional Playwright project should not use XPath for everything. Choose the locator based on the application's stability and the meaning of the element.

Situation Preferred Approach Why
User-facing button get_by_role() Expresses user intent
Form field with label get_by_label() Clear and semantic
Visible message get_by_text() Directly expresses visible text
Stable automation attribute get_by_test_id() Explicit automation contract
Complex DOM relationship XPath Strong relationship/navigation capability
Stable CSS structure CSS locator Simple structural targeting
28. Common XPath Mistakes
29. Bad XPath vs Maintainable XPath

❌ Fragile

/html/body/div[2]/div[3]/div[1]/button[2]

Depends heavily on the exact DOM hierarchy and position.

✓ More Stable

//button[
    normalize-space()='Submit Order'
]

Uses meaningful information exposed by the element.

30. Why Playwright Can Feel More Powerful Than Selenium for XPath Work

XPath itself is not inherently more powerful in Playwright. Selenium also supports XPath very well.

The practical difference is the surrounding locator ecosystem.

1. Locator API

XPath fits into Playwright's unified locator() model.

2. Locator Composition

You can combine locators, filters and nested locators instead of putting every relationship into one XPath.

3. Actionability

Playwright actions perform relevant actionability checks before interacting with elements.

4. Web-First Assertions

Locator assertions can retry while the application reaches the expected state.

5. Semantic Locators

XPath can be combined conceptually with role, text, label and test-id based locator strategies.

6. Less Synchronization Code

Many common interactions require less explicit waiting code than a manually synchronized Selenium workflow.

31. Professional Mental Model
User Meaning
→
Role / Label / Text
→
Test ID / CSS
→
XPath

Think of XPath as a powerful tool in your locator toolbox, not as the only locator strategy.

Recommended approach: Start with the most meaningful and stable locator. If a user-facing role, label or text describes the target clearly, use the corresponding Playwright locator. If the element must be located through a complex DOM relationship, XPath can be an appropriate solution.
32. XPath Quick Reference
Playwright Python
# Tag
page.locator("//button")

# ID
page.locator("//input[@id='email']")

# Attribute
page.locator("//input[@name='email']")

# Multiple attributes
page.locator(
    "//input[@type='email' and @required]"
)

# Exact text
page.locator(
    "//button[text()='Login']"
)

# Normalized text
page.locator(
    "//button[normalize-space()='Login']"
)

# Partial text
page.locator(
    "//button[contains(text(),'Login')]"
)

# Partial attribute
page.locator(
    "//button[contains(@id,'submit')]"
)

# Parent
page.locator(
    "//input[@name='email']/parent::*"
)

# Ancestor
page.locator(
    "//span[normalize-space()='John']"
    "/ancestor::div[contains(@class,'card')]"
)

# Following sibling
page.locator(
    "//label[normalize-space()='Email']"
    "/following-sibling::input[1]"
)

# Specific row
page.locator(
    "//tr[.//td[normalize-space()='ORD-1001']]"
)

# Element inside specific row
order = page.locator(
    "//tr[.//td[normalize-space()='ORD-1001']]"
)

order.locator(
    ".//button[normalize-space()='View']"
).click()
33. XPath Best Practices Checklist
34. Interview Questions

1. Does Playwright support XPath?

Yes. XPath can be used through Playwright's locator() API.

2. Is XPath more powerful in Playwright than Selenium?

The XPath language itself is not more powerful in Playwright. Both support XPath. Playwright can provide a different overall automation experience through its locator model, actionability, locator composition and web-first assertions.

3. Should XPath be used for every Playwright locator?

No. Use the simplest stable locator that communicates the target clearly.

4. When is XPath especially useful?

Complex DOM relationships, table rows, parent/ancestor relationships, sibling relationships and certain dynamic attribute scenarios are common examples.

5. What should be avoided?

Avoid fragile absolute XPath, unnecessary indexes, unstable generated attributes and unnecessarily complicated expressions.

Final Takeaway
Remember: XPath is a powerful locator language and Playwright supports it well. The key Playwright advantage is not that XPath syntax is different from Selenium. The practical advantage comes from using XPath inside Playwright's broader locator architecture.
Simple UI
→
Role / Label / Text
→
Test ID / CSS
→
XPath for Complex Relationships

Use XPath when the relationship matters. Use semantic locators when the user's intent is clearer. Build locators for stability and readability.