📖 The Story: The Legacy Table Nightmare

Meet Priya. She joined a company with a 10-year-old web application built with server-rendered HTML tables. No React. No Vue. No semantic HTML. No ARIA roles. No data-testid attributes. Just <tr>, <td>, and dynamically generated class names like .cell_X7A2 that changed every build.

getByRole? No buttons with roles. getByLabel? No labels. getByTestId? No test IDs existed. CSS selectors? The classes were hashed gibberish. Priya's only option was XPath—the only tool powerful enough to traverse that table and find "the cell in the 3rd row that contains 'Total' and then click the button in the adjacent cell."

XPath is your tool of last resort. But when you need it, nothing else will do.

🎯 When to Use XPath

🏚️

Legacy Systems

Old apps with no semantic HTML, no ARIA roles, no test IDs.

🔀

DOM Traversal

Moving UP the DOM tree (parent, ancestor) — impossible with CSS.

🔍

Complex Text

Finding elements by partial text when getByText isn't enough.

🧒 Explain Like I'm 10

CSS selectors are like using a street address (123 Main Street, Apt 4B). XPath is like giving directions using landmarks (Go to the building with the red door, then turn left to the 3rd room on the right). Street addresses are faster and more reliable—but sometimes a building doesn't have an address, and you need landmarks.

🎓 Professional Explanation

XPath (XML Path Language) is a query language for selecting nodes from an XML/HTML document. Playwright supports XPath via page.locator('xpath=//...') or implicitly when a string starts with // or ... XPath is more powerful than CSS selectors but slower to evaluate because the browser engine must compile and execute the XPath query rather than using the native CSS selector engine.

⚖️ Absolute vs Relative XPath

FeatureAbsolute XPathRelative XPath
Starts with/html/body/...//
LengthVery longShort
Resilience❌ Breaks with any DOM change✅ Survives most DOM changes
SpeedSlower (traverses from root)Faster (searches anywhere)
Recommendation❌ Never use✅ Always use
typescript
// ❌ Absolute XPath — NEVER use this
page.locator('/html/body/div[2]/form/table/tr[3]/td[1]/input')

// ✅ Relative XPath — Use this
page.locator('//input[@id="email"]')

📋 XPath Syntax Reference

SyntaxWhat It DoesExample
//tagnameFind all elements with tag//button
//tagname[@attr='val']By attribute//input[@type='submit']
//*[@attr='val']Any element with attribute//*[@data-role='admin']
//tagname[contains(@attr, 'val')]Partial attribute match//div[contains(@class, 'active')]
//tagname[text()='Val']Exact text match//button[text()='Login']
//tagname[contains(text(), 'Val')]Partial text match//h2[contains(text(), 'Welcome')]
//tag1//tag2Descendant//form//input
//tag1/tag2Direct child//div/button
..Parent element//input[@id='x']/..
following-sibling::Next siblings//td[text()='Label']/following-sibling::td
preceding-sibling::Previous siblings//td[text()='Value']/preceding-sibling::td

🎭 Using XPath in Playwright

typescript
// Method 1: Explicit xpath= prefix (recommended for clarity)
await page.locator('xpath=//button[text()="Submit"]').click();

// Method 2: Implicit (string starts with // or ..)
await page.locator('//button[text()="Submit"]').click();

// Find row by text, then click button in adjacent cell
await page.locator('//tr[td[text()="John Doe"]]//button[text()="Edit"]').click();

// Find element by partial text using contains
await page.locator('//*[contains(text(), "Welcome back")]').isVisible();

// Traverse UP to parent (CSS can't do this!)
await page.locator('//input[@id="email"]/..').evaluate(el => el.tagName);

When to use xpath= prefix: Always. It makes your intent explicit and prevents confusion with CSS selectors. A string starting with // works implicitly, but xpath= is self-documenting.

⚠️ Common Mistakes

Mistake 1: Using Absolute XPath from Browser DevTools

typescript · ❌ Never do this
// ❌ Chrome DevTools "Copy XPath" gives absolute paths
page.locator('/html/body/div[3]/div[1]/section[2]/ul/li[5]/a')

// ✅ Write a relative XPath instead
page.locator('xpath=//a[contains(text(), "Contact Us")]')

Mistake 2: Using XPath for Things CSS Does Better

typescript · ❌ Overkill
// ❌ XPath for simple ID lookup — CSS is faster
page.locator('xpath=//*[@id="submit"]')

// ✅ CSS for ID — native and faster
page.locator('#submit')

Mistake 3: Fragile Index-Based XPath

typescript · ❌ Brittle
// ❌ Breaks if a new row is added
page.locator('xpath=//tr[3]/td[2]')

// ✅ Find by content, not position
page.locator('xpath=//tr[td[text()="John Doe"]]/td[2]')

✅ Best Practices

  1. Use XPath only as a last resort — prefer getByRole, getByText, CSS, then XPath.
  2. Always use relative XPath (starts with //), never absolute.
  3. Always use xpath= prefix for explicitness.
  4. Prefer contains() over exact match for text and classes to survive minor changes.
  5. Avoid index-based selectors (//div[3]) — use content-based matching instead.
  6. Use XPath for DOM traversal (parent, sibling) where CSS can't reach.
  7. If you're using XPath for everything, refactor your app — add data-testid or ARIA roles.

🏗️ Senior Engineer Deep Dive

Why Playwright Discourages XPath

Playwright's official guidance is clear: Avoid XPath. The reasons are architectural:

  • Performance: XPath queries are compiled and evaluated by a separate engine in the browser. CSS selectors use the browser's native querySelector engine, which is heavily optimized. In benchmarks, CSS selectors are 2-5x faster.
  • Strictness: Playwright locators (getByRole, etc.) are strict by default. XPath strings are not — they can silently match multiple elements or the wrong element.
  • Resilience: XPath is tied to DOM structure. Semantic locators are tied to user-perceivable behavior. DOM structure changes frequently; user behavior doesn't.

When XPath Is the ONLY Option

There is exactly one scenario where XPath is superior: traversing UP the DOM tree. CSS selectors can only traverse down (parent > child, ancestor descendant). XPath can traverse in any direction using axes like parent::, ancestor::, following-sibling::, and preceding-sibling::. If you need to "find the parent form of this input" or "find the cell next to this label," XPath is your only option.

💼 Interview Questions

Beginner
What is the difference between absolute and relative XPath?
Show answer
Absolute XPath starts from the root element (/html/body/...) and traces the full path. It breaks with any DOM change. Relative XPath starts with // and searches anywhere in the document, making it much more resilient to DOM restructuring.
Intermediate
When should you use XPath instead of CSS selectors?
Show answer
Use XPath when: 1) You need to traverse UP the DOM tree (parent/ancestor), which CSS cannot do. 2) You need to find elements by text content (though Playwright's getByText handles most cases). 3) You're working with legacy systems that lack semantic attributes, test IDs, or ARIA roles.
Advanced
Why does Playwright recommend against XPath, and what is the performance implication?
Show answer
XPath queries use a separate compilation and evaluation engine, whereas CSS selectors leverage the browser's highly-optimized native querySelector engine. XPath is typically 2-5x slower. Additionally, XPath strings don't benefit from Playwright's strict locator model and are more brittle because they tie tests to DOM structure rather than user-perceivable behavior.

🏋️ Practical Exercises

🛠️ Hands-On Intermediate

XPath Scavenger Hunt

  1. Navigate to https://demo.playwright.dev/todomvc.
  2. Write an XPath that finds the input by its placeholder using contains().
  3. Add 3 todos. Write an XPath that finds the second todo item by its text.
  4. Write an XPath that finds the count text ("3 items left") using contains(text(), 'items').
  5. Write an XPath that traverses from a todo item UP to its parent li using /...

🧠 Quiz

1. What character does a relative XPath start with?
/
//
./
*
2. Why should you avoid absolute XPath?
It's too fast
It breaks with any DOM change
It doesn't support attributes
Playwright doesn't support it
3. What does //input[@id='email']/.. select?
The input with id 'email'
The parent element of the input with id 'email'
All inputs inside email div
The next sibling
4. Which XPath function matches partial text?
text()
contains()
starts-with()
equals()
5. What is the recommended prefix for XPath in Playwright?
select=
xpath=
path=
No prefix needed
6. What can XPath do that CSS selectors cannot?
Find by ID
Find by class
Traverse up to parent elements
Find by attribute
7. Which is faster: CSS selectors or XPath?
CSS selectors
XPath
They are equal
Depends on browser
8. What does //*[contains(@class, 'active')] find?
Elements with exact class 'active'
Any element whose class attribute contains 'active'
Only div elements with class 'active'
The first active element
9. What does following-sibling:: do in XPath?
Finds child elements
Finds elements at the same level that come after the current node
Finds the parent element
Finds previous siblings
10. What is the priority order for locators in Playwright?
XPath > CSS > getByTestId > getByRole
getByRole > getByText > getByLabel > getByTestId > CSS > XPath
CSS > XPath > getByRole
getByTestId > getByRole > XPath

❓ FAQ

Can I use XPath with Playwright's built-in locator methods like getByRole?
Show answer
No. XPath is used exclusively with page.locator('xpath=//...'). The built-in methods (getByRole, getByText) use their own engines and don't accept XPath syntax.
Should I use Chrome DevTools' "Copy XPath" feature?
Show answer
No. DevTools almost always generates absolute XPath (/html/body/div[3]/...). These are extremely brittle and break with any DOM change. Always write relative XPath manually.
Is there a difference between xpath=//button and just //button in Playwright?
Show answer
Functionally, no. Playwright automatically detects strings starting with // or .. as XPath. However, using the xpath= prefix is recommended because it makes your intent explicit and self-documenting.

📌 Summary

🎯 Key Takeaways

  • XPath is a last-resort tool — prefer getByRole, getByText, CSS selectors first.
  • Always use relative XPath (//), never absolute (/html/body/...).
  • Use xpath= prefix for explicit, self-documenting locators.
  • XPath's superpower: DOM traversal in any direction (parent, sibling, ancestor).
  • CSS is faster — XPath uses a separate evaluation engine.
  • Use contains() for partial text and attribute matching.
  • Avoid index-based XPath (//div[3]) — match by content instead.
  • If you use XPath everywhere, refactor your app — add test IDs and ARIA roles.