Selenium XPath: Complete Guide to Finding Elements
XPath is a query language for selecting nodes in an XML/HTML document. In Selenium, it's used to locate elements that CSS selectors can't easily target — parent elements, elements by text content, or elements based on sibling conditions. This guide covers XPath syntax, practical patterns, and when to use XPath vs CSS selectors.
Key Takeaways
Use CSS selectors first. CSS selectors are faster, more readable, and sufficient for most element lookups. Only reach for XPath when CSS can't do the job — like selecting a parent or finding by text content.
Never use absolute XPath. /html/body/div[2]/form/input[1] breaks on every page change. Use relative XPath starting with // and targeting meaningful attributes.
contains() is your most useful XPath function. It handles partial matches on class names, text, and attribute values — making selectors more resilient than exact-match comparisons.
Find elements by text with text() and contains(text(), ...). This is XPath's unique advantage over CSS selectors — CSS can't select elements based on their text content.
Avoid complex XPath in production tests. Long XPath expressions with multiple conditions are hard to read and break easily. If XPath gets complex, consider adding a data-testid attribute to the element instead.
What Is XPath?
XPath (XML Path Language) is a query language for selecting nodes in XML and HTML documents. In Selenium, XPath is one of the strategies you can use to locate elements on a web page.
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("https://example.com")
# Using XPath to find an element
element = driver.find_element(By.XPATH, "//button[@id='submit']")
element.click()
XPath vs CSS Selectors
Before diving into XPath, understand when to use it vs CSS selectors:
| CSS Selector | XPath | |
|---|---|---|
| Syntax | Simpler | More verbose |
| Speed | Faster | Slightly slower |
| Select by text | ❌ No | ✅ Yes |
| Select parent element | ❌ No | ✅ Yes |
| Select by attribute | ✅ Yes | ✅ Yes |
| Navigate siblings | Limited | ✅ Full support |
| Browser DevTools copy | ✅ Supported | ✅ Supported |
| Readability | Higher | Lower |
Rule of thumb: Use CSS selectors by default. Use XPath when you need:
- To select an element by its text content
- To navigate to a parent or ancestor element
- Complex sibling/position conditions
XPath Syntax Basics
Absolute vs Relative XPath
Absolute XPath — starts from the root, fragile:
/html/body/div/form/input ❌ Breaks if any parent changes
Relative XPath — starts from anywhere in the document, resilient:
//input[@name='email'] ✅ Finds the input anywhere on the page
Always use relative XPath starting with //.
Selecting Elements
//tagname Any element with that tag
//div All <div> elements
//* All elements (wildcard)
Selecting by Attribute
//input[@type='submit'] Exact attribute match
//div[@class='product-card'] Exact class match
//a[@href] Has the attribute (any value)
//input[@data-testid] Has data-testid attribute
Combining Tag and Attribute
//button[@type='submit']
//input[@name='email' and @type='text']
//div[@id='results' and @class='active']
XPath Axes
Axes navigate relative to a context node. Most useful ones:
Selecting Ancestors and Descendants
//span/parent::div Direct parent
//span/ancestor::div Any ancestor that's a div
//span/ancestor-or-self::div Ancestor or the span itself
//form/descendant::input All inputs inside form
//div[@class='card']//button Shorthand for descendant
Selecting Siblings
//h2/following-sibling::p Next <p> siblings after h2
//li[3]/preceding-sibling::li <li> elements before the 3rd li
//dt/following-sibling::dd[1] First <dd> after a <dt>
Practical Parent Selection Example
CSS selectors can't select parent elements. XPath can:
# Find a button by text, then get its parent container
parent = driver.find_element(
By.XPATH, "//button[text()='Add to Cart']/parent::div"
)
Selecting by Text Content
This is XPath's biggest advantage over CSS selectors:
Exact Text Match
//h1[text()='Welcome Back']
//button[text()='Submit']
//a[text()='Click here']
Partial Text Match (contains)
//button[contains(text(), 'Add')]
//p[contains(text(), 'Error')]
//span[contains(text(), 'USD')]
Normalize Whitespace
Text with extra whitespace breaks exact matches. Use normalize-space():
//h1[normalize-space(text())='Welcome Back']
//button[normalize-space()='Submit']
Case-Insensitive Text (XPath 1.0)
XPath 1.0 (used in browsers) doesn't have lower-case(). Workaround:
# Find a button regardless of case using translate()
xpath = "//button[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'submit')]"
Or just use Selenium's explicit case-aware search:
buttons = driver.find_elements(By.TAG_NAME, "button")
target = next(b for b in buttons if "submit" in b.text.lower())
XPath Functions Reference
String Functions
text() Text content of element
normalize-space(text()) Text with extra whitespace removed
contains(str, substr) True if string contains substring
starts-with(str, prefix) True if string starts with prefix
string-length(str) Length of string
concat(str1, str2) Concatenate strings
Examples
//input[contains(@placeholder, 'Search')]
//div[starts-with(@id, 'product-')]
//p[string-length(text()) > 100]
Position Functions
//li[1] First li element
//li[last()] Last li element
//li[last()-1] Second to last
//li[position() > 2] From 3rd onward
//tr[position() mod 2 = 0] Even rows
Count Functions
//ul[count(li) > 5] ul with more than 5 li children
//div[count(*)=0] Empty div (no children)
Common XPath Patterns in Selenium
Find by ID (equivalent to CSS #id)
# XPath
driver.find_element(By.XPATH, "//*[@id='submit-btn']")
# CSS (faster, prefer this)
driver.find_element(By.CSS_SELECTOR, "#submit-btn")
# Or directly:
driver.find_element(By.ID, "submit-btn")
Find by Class (partial match)
# XPath — handles multiple classes
driver.find_element(By.XPATH, "//div[contains(@class, 'product-card')]")
# CSS equivalent
driver.find_element(By.CSS_SELECTOR, "div.product-card")
Find Input by Placeholder
driver.find_element(By.XPATH, "//input[@placeholder='Enter your email']")
Find Button by Text
driver.find_element(By.XPATH, "//button[text()='Sign In']")
# Partial text
driver.find_element(By.XPATH, "//button[contains(text(), 'Sign')]")
Find Link by Text
driver.find_element(By.XPATH, "//a[text()='Learn More']")
# Or use Selenium's built-in:
driver.find_element(By.LINK_TEXT, "Learn More")
driver.find_element(By.PARTIAL_LINK_TEXT, "Learn")
Find Table Cell by Column Header
# Find the price cell in the row where the name is "Widget"
driver.find_element(
By.XPATH,
"//tr[td[1][text()='Widget']]/td[3]"
)
Find Form Field by Label
# Find input associated with label "Email Address"
driver.find_element(
By.XPATH,
"//label[contains(text(), 'Email')]/following-sibling::input"
)
# Or via the 'for' attribute matching the input id
driver.find_element(
By.XPATH,
"//input[@id=//label[text()='Email Address']/@for]"
)
Find Element in a Specific Section
# Find the submit button inside the checkout form specifically
driver.find_element(
By.XPATH,
"//form[@id='checkout-form']//button[@type='submit']"
)
Python Examples
from 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()
wait = WebDriverWait(driver, 10)
driver.get("https://example-shop.com/products")
# Wait for products to load
wait.until(EC.presence_of_element_located((By.XPATH, "//div[@class='product-list']")))
# Get all product names
products = driver.find_elements(By.XPATH, "//div[contains(@class,'product-card')]//h2")
names = [p.text for p in products]
print(names)
# Click the second product
second_product = driver.find_element(By.XPATH, "(//div[contains(@class,'product-card')])[2]")
second_product.click()
# Get price with currency symbol stripped
price_text = driver.find_element(By.XPATH, "//span[@class='price']").text
price = float(price_text.replace("$", "").replace(",", ""))
driver.quit()
Java Examples
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import java.util.List;
WebDriver driver = new ChromeDriver();
driver.get("https://example.com");
// Find by text
WebElement button = driver.findElement(By.xpath("//button[text()='Submit']"));
// Find by partial attribute
WebElement input = driver.findElement(By.xpath("//input[contains(@name, 'email')]"));
// Find all matching elements
List<WebElement> rows = driver.findElements(By.xpath("//tr[@class='data-row']"));
// Parent navigation
WebElement container = driver.findElement(
By.xpath("//span[@class='product-name'][text()='Widget Pro']/ancestor::div[@class='product-card']")
);
Getting XPath in Browser DevTools
You can copy XPath directly from Chrome DevTools:
- Right-click an element → Inspect
- In the Elements panel, right-click the highlighted HTML
- Select Copy → Copy XPath or Copy → Copy full XPath
The "Copy XPath" option gives you a relative XPath. The "Copy full XPath" gives an absolute path (avoid this).
Warning: DevTools-generated XPath is often fragile. It may use positional selectors like div[3] that break when the page structure changes. Always simplify DevTools XPath by targeting meaningful attributes.
Debugging XPath in Browser Console
Test XPath expressions in the browser console before writing Selenium code:
// Evaluate XPath in browser console
$x("//button[text()='Submit']") // Chrome DevTools shorthand
$x("//div[contains(@class, 'product-card')]")
// Standard JS (works in any browser)
document.evaluate(
"//button[text()='Submit']",
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
Common XPath Mistakes
Using Absolute XPath
❌ /html/body/div[1]/div[2]/form/div/input[3]
✅ //input[@name='password']
Exact Class Match (fails with multiple classes)
❌ //div[@class='product'] (fails if class is "product active")
✅ //div[contains(@class,'product')] (works with any class combination)
Not Handling Whitespace in Text
❌ //h2[text()='Product Name'] (fails if there's " Product Name " with spaces)
✅ //h2[normalize-space()='Product Name']
Selecting Multiple Elements When Expecting One
# This finds ALL matching elements — only use first one
elements = driver.find_elements(By.XPATH, "//button[@type='submit']")
# Make XPath more specific:
element = driver.find_element(By.XPATH, "//form[@id='login']//button[@type='submit']")
The Better Long-Term Solution
XPath in test automation creates maintenance burden: selectors break when developers refactor HTML structure, and teams spend time updating tests instead of building features.
HelpMeTest takes a different approach. Instead of writing XPath, you describe test actions in plain English:
*** Test Cases ***
User Adds Product to Cart
Go To https://example-shop.com
Click Widget Pro
Click Add to Cart
Should See Cart: 1 item
HelpMeTest generates and maintains the element selectors automatically. When the UI changes, the AI self-heals the selectors — no manual XPath updates.
When to use Selenium XPath:
- You have an existing Selenium test suite
- You need fine-grained control over element selection
- You're building testing infrastructure
When AI testing is better:
- New test automation projects
- Apps that change frequently
- When test maintenance is a bigger problem than writing tests
XPath Quick Reference
// descendants from anywhere
/ direct child
. current node
.. parent node
* any element
@attr attribute
//div[@id='x'] ID
//div[@class='x'] Exact class
//div[contains(@class,'x')] Class contains
//a[text()='x'] Exact text
//a[contains(text(),'x')] Text contains
//p[normalize-space()='x'] Text (whitespace-tolerant)
//input[@placeholder] Has attribute
//li[1] First
//li[last()] Last
//li[position()>2] From 3rd
//span/parent::div Direct parent
//span/ancestor::div Any ancestor div
//h2/following-sibling::p Next p siblings
Try HelpMeTest free — write tests in plain English without managing selectors.