Selenium XPath: Complete Guide to Finding Elements

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')]")
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:

  1. Right-click an element → Inspect
  2. In the Elements panel, right-click the highlighted HTML
  3. 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.

Read more