Selenium XPath Guide
Battle-tested XPath patterns for Selenium WebDriver and Playwright. Stop guessing selectors—validate them, highlight matches, and ship stable tests.
Locator checklist
- Use data-test-id or aria-label when available.
- Combine contains() with tag scopes to avoid over-matching.
- Prefer relative selectors scoped to a stable parent.
- Minimize reliance on index-based predicates unless the list is stable.
High-value patterns
//button[normalize-space()="Save"]— resilient CTA matching//input[@name="email" or @id="email"]— tolerate differing attributes//table//tr[td[contains(., "Active")]]//td[3]— scoped cell lookup//div[contains(@class, "error") and not(contains(@class, "hidden"))]— visible errors
Anti-patterns to avoid
- Absolute paths like /html/body/div[3]/div[2] — break when layout changes.
- Text-only selectors on unstable copy—pair with attributes when possible.
- Complex chained indexes in dynamic lists—prefer attribute beacons.
- Ignoring wait conditions—wrap clicks in waits to avoid flaky tests.
Copy-ready patterns
Buttons and links
Exact text
//button[normalize-space(text())='Submit']
Partial text
//a[contains(text(),'Learn More')]
CTA by data-test-id
//button[@data-test-id='cta-primary']
Forms and inputs
By name
//input[@name='email']
Placeholder
//input[@placeholder='Enter email']
Label + input
//label[text()='Email']/following-sibling::input
Checkbox checked
//input[@type='checkbox' and @checked]
Dynamic content
Dynamic ID
//div[starts-with(@id, 'react-select')]
Toast message
//div[contains(@class,'toast') and contains(text(),'Saved')]
Loading finished
//*[contains(@class,'loading')][not(@hidden)]/following::*[1]
Tables
Cell in row
//table[@id='orders']//tr[3]//td[2]
Row by text
//table//tr[td[contains(text(),'Paid')]]
Column header
//table//th[normalize-space()='Status']
Language snippets
Python + Selenium
Copy & adaptfrom selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example.com")
login_btn = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//button[normalize-space()='Login']"))
)
login_btn.click()
rows = driver.find_elements(By.XPATH, "//table[@id='orders']//tr[td[contains(., 'Active')]]")
for row in rows:
email = row.find_element(By.XPATH, ".//td[2]").text
print(email)
driver.quit()JavaScript + Playwright
Copy & adaptimport { chromium } from "playwright";
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto("https://example.com");
const cta = page.getByRole("button", { name: "Start free trial" });
await cta.click();
const statusCells = page.locator("xpath=//table//tr[td[contains(., 'Paid')]]//td[3]");
const statuses = await statusCells.allTextContents();
console.log(statuses);
await browser.close();Java + Selenium
Copy & adaptWebDriver driver = new ChromeDriver();
driver.get("https://example.com");
WebElement searchBox = driver.findElement(By.xpath("//input[@name='q']"));
searchBox.sendKeys("xpath");
List<WebElement> links = driver.findElements(By.xpath("//a[contains(text(),'XPath')]"));
for (WebElement link : links) {
System.out.println(link.getAttribute("href"));
}
driver.quit();C# + Selenium
Copy & adaptusing OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
var driver = new ChromeDriver();
driver.Navigate().GoToUrl("https://example.com");
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
var submit = wait.Until(drv => drv.FindElement(By.XPath("//button[@type='submit' or @class='submit-btn']")));
submit.Click();
var alerts = driver.FindElements(By.XPath("//div[contains(@class,'alert') and not(contains(@class,'hidden'))]"));
foreach (var alert in alerts)
{
Console.WriteLine(alert.Text);
}
driver.Quit();Best practices for stability
- Prefer semantic attributes over positional selectors. Reach for @data-test-id, @aria-label, and stable IDs.
- Normalize whitespace when matching text: normalize-space() handles trailing spaces and line breaks.
- Avoid absolute paths that start at /html/body; prefer short, scoped selectors under a stable container.
- Use contains() and starts-with() for dynamic attributes instead of brittle equality checks.
- In tables, filter rows first, then drill into cells: //tr[td[contains(.,'Active')]]//td[3].
- Combine axes to avoid flakiness: label[text()='Email']/following-sibling::input is sturdier than //input[1].
- Keep locators readable—future maintainers should understand intent without comments.
Fast path:
Use the XPath playground to test each locator before merging. Pair it with the cheat sheet for syntax and the DevTools guide to debug in-browser.