XPath is the most flexible locator strategy in Selenium. When element IDs are dynamic, class names are shared, or the DOM structure is complex, XPath gives you the precision to target exactly what you need. This cheat sheet covers everything from basic syntax to advanced axes, operators, string functions, and real-world patterns for test automation.
Quick Reference: Jump to a Section
- XPath Syntax and Path Expressions
- Absolute vs. Relative XPath
- Predicates and Indexing
- Wildcards and Multi-Path Selection
- XPath Axes
- Operators
- String and Math Functions
- Selectors
- Dynamic XPath Strategies
- Using XPath in Selenium
- Testing XPath in Browser DevTools
- XPath vs. CSS Selectors
- Common XPath Mistakes
- XPath with jQuery
Free XPath Cheat Sheet (PDF)
Download our printable XPath cheat sheet covering syntax, all 13 axes, operators, string functions, dynamic locator patterns, and Selenium usage examples for Python and Java.
Download the XPath Cheat Sheet PDFXPath Syntax and Path Expressions
XPath (XML Path Language) uses a path-like syntax to identify and navigate nodes in XML and HTML documents. In Selenium, you use XPath to locate WebElements in the DOM.
The standard syntax for an XPath expression in Selenium:
XPath = //tagname[@attribute="value"]
The core path expressions for selecting nodes in an XML or HTML document:
| Expression | Description |
|---|---|
nodename |
Selects all elements with that node name |
/ |
Selects from the root node |
// |
Selects matching elements anywhere in the document |
. |
Selects the current node |
.. |
Selects the parent of the current node |
@ |
Selects attributes |
Common Step Expressions
| XPath | Description |
|---|---|
//div |
Select all div elements anywhere in the document |
//[@id='btn'] |
Select all elements with ID 'btn' |
//div[@name='post'] |
Select all div elements with name attribute 'post' |
//a/text() |
Get the text content of all anchor tags |
//a/@href |
Get the href attribute value of all anchor tags |
Prefix Expressions
| Prefix | Example | Description |
|---|---|---|
// |
//p[@class='footer'] |
Select paragraph tag from anywhere in the DOM |
/ |
/a |
Find all anchor tags from the given node |
/ |
/html/body/div |
Find all div elements starting from the root element |
Absolute vs. Relative XPath
You have two ways to locate a WebElement in the DOM: absolute path and relative path.
Absolute XPath
Absolute XPath specifies the complete path from the root element to the target element. It begins with a single forward slash (/), indicating selection from the document root.
/html/body/div[x]/div[y]/Example:
/html//div/div/div/div[1]/div/a/imgThe major downside: any change to the DOM structure breaks the XPath. Avoid absolute paths in production test scripts.
Relative XPath
Relative XPath locates an element based on its position relative to another element rather than the full document path. It begins with a double forward slash (//).
//tagname[@attribute='value']Relative XPath is the standard choice for Selenium automation. Changes to the page hierarchy outside the targeted element do not break the locator.
Selecting Nodes
| XPath | Description |
|---|---|
div |
Selects all div elements |
/div |
Selects div element from the root element |
div/tr |
Selects all tr elements that are direct children of div |
//tr |
Selects all tr elements anywhere in the document |
div//tr |
Selects all tr elements that are descendants of div, at any depth |
//@class |
Selects all class attributes in the document |
Predicates and Indexing
Predicates let you target a specific node within a set. They appear in square brackets and return a boolean (true or false). You can combine them with relational and boolean operators.
| XPath | Description |
|---|---|
/div/a[1] |
Selects the first anchor element within the div |
/div/a[last()] |
Selects the last anchor element within the div |
/div/a[last()-1] |
Selects the second-to-last anchor element within the div |
/div/a[position()<3] |
Selects the first two anchor elements within the div |
//a[@class] |
Selects all anchor elements that have a class attribute |
//a[@class='btn'] |
Selects all anchor elements with class attribute value 'btn' |
/div/h1[1]/a |
Selects all anchors within the first h1 that is a child of div |
/div/h1/a[1] |
Selects the first anchor within each h1 inside the div |
Predicate Examples with Attributes
| Expression | Description |
|---|---|
element_name[N] |
Selects element at position N (1-based index) |
empinfo/employee[2] |
Selects the second employee element under empinfo |
empinfo/employee[2]/name |
Selects the name child of the second employee |
empinfo/employee[@id] |
Selects all employee elements that have an id attribute |
empinfo/employee[@id=2] |
Selects employee elements where id equals 2 |
empinfo/employee[@id=2][2] |
Selects the second employee element with id of 2 |
empinfo/employee[1][@id=1] |
Selects the first employee element with id of 1 |
//designation[@discipline and @experience] |
Selects designation elements that have both attributes |
//designation[@discipline or @experience] |
Selects designation elements that have either attribute |
Chaining Order Matters
The order of predicates changes the result. These two expressions are not equivalent:
a[1][@href='/'] // The first anchor, only if it has href='/'
a[@href='/'][1] // The first anchor that has href='/'Indexing
| XPath | Description |
|---|---|
//a[1] |
Selects the first anchor tag |
//a[last()] |
Selects the last anchor tag |
//ul/li[2] |
Selects the second li that is a child of ul |
//ul/li[position()=2] |
Selects the second li that is a child of ul (using position) |
//ul/li[position()>1] |
Selects all li elements that are not the first child of ul |
Wildcards and Multi-Path Selection
Wildcard Expressions
| Expression | Description |
|---|---|
* |
Matches any HTML element |
@* |
Matches any attribute of an element |
node() |
Matches any kind of node |
/div/* |
Selects all child elements of a div |
//* |
Selects all elements in the HTML document |
//a[@*] |
Selects all anchor elements that have any attribute |
. |
Matches the current node context |
.. |
Refers to the parent context node |
text() |
Selects all text node children of the current element |
Multi-Path Selection with the Union Operator
Use the | operator to combine multiple XPath expressions into a single query:
| XPath | Description |
|---|---|
//div | //a |
Selects all div and anchor elements in the document |
//div/h1 | //div/a |
Selects all h1 and anchor elements within a div |
XPath Axes
XPath defines 13 axes that describe the relationship between the current context node and other nodes in the document. Axes let you navigate the DOM tree in any direction from any node.
Syntax:
AxisName::nodetest[predicate]All 13 Axes
| Axis | Shorthand | Description |
|---|---|---|
self |
. |
Selects the context node itself |
child |
/ |
Selects direct children of the context node |
descendant |
// |
Selects all descendants at any depth |
descendant-or-self |
// |
Selects all descendants plus the context node |
parent |
.. |
Selects the parent of the context node |
ancestor |
Selects all ancestors up to the root node | |
ancestor-or-self |
Selects all ancestors plus the context node | |
attribute |
@ |
Selects attributes of the context node |
following |
Selects all nodes after the context node in document order (excluding attributes and namespaces) | |
following-sibling |
Selects all siblings that follow the context node | |
preceding |
Selects all nodes before the context node in document order (excluding attributes and namespaces) | |
preceding-sibling |
Selects all siblings that precede the context node | |
namespace |
Selects all namespace nodes of the context node |
Axis Usage Examples
| XPath | Description |
|---|---|
//name/self::* |
Selects the name context node itself |
child::* |
Selects all child nodes of the context node |
child::node() |
Selects all child nodes of any type |
//employee/descendant::* |
Selects all descendants of the employee node |
//employee/descendant-or-self::* |
Selects all descendants of employee plus the node itself |
//employee/ancestor::* |
Selects all ancestors of the employee node |
//employee/ancestor-or-self::* |
Selects all ancestors plus the employee node itself |
//name/parent::* |
Selects the parent of the name node |
//name/parent::employee |
Returns the parent if it is an employee element, otherwise nothing |
//attribute::id |
Selects all nodes with an id attribute |
//attribute::* |
Selects all nodes with any attribute |
//employee[@id=1]/following::* |
Selects all nodes after employee with id 1 |
//employee[@id=1]/following-sibling::* |
Selects all sibling nodes after employee with id 1 |
//employee[@id=3]/preceding::* |
Selects all nodes before employee with id 3 |
//employee[@id=3]/preceding-sibling::* |
Selects all sibling nodes before employee with id 3 |
//name/ancestor-or-self::employee |
Selects the employee ancestor plus the name node itself |
XPath Operators
XPath expressions can return a number, boolean, node-set, or string. The following operators let you manipulate and evaluate these values.
All Operators
| Operator | Description |
|---|---|
| |
Computes the union of two node-sets |
+ |
Addition |
- |
Subtraction |
* |
Multiplication |
div |
Division |
mod |
Modulus (division remainder) |
= |
Equal |
!= |
Not equal |
< |
Less than |
<= |
Less than or equal to |
> |
Greater than |
>= |
Greater than or equal to |
or |
Logical OR (either condition must be true) |
and |
Logical AND (both conditions must be true) |
not() |
Negates the condition |
Boolean Operators
| Operator | Description |
|---|---|
and |
Both conditions must be satisfied |
or |
At least one condition must be satisfied |
not() |
Condition must not be satisfied |
String Functions
String functions are among the most useful XPath tools in Selenium, particularly for locating elements with dynamic or partial attribute values.
| Function | Description |
|---|---|
contains(string1, string2) |
Returns true if string1 contains string2 |
starts-with(string1, string2) |
Returns true if string1 starts with string2 |
ends-with(string1, string2) |
Returns true if string1 ends with string2 |
substring(string, offset, length) |
Returns a section of string starting at offset for the given length |
substring-before(string1, string2) |
Returns the part of string1 before the first occurrence of string2 |
substring-after(string1, string2) |
Returns the part of string1 after the first occurrence of string2 |
string-length(string) |
Returns the number of characters in the string |
normalize-space(string) |
Strips leading and trailing whitespace, collapses internal whitespace to single spaces |
translate(string1, string2, string3) |
Replaces characters in string1 that match string2 with the corresponding characters in string3 |
concat(string1, string2, ...) |
Concatenates all provided strings |
format-number(number, format, locale) |
Returns a formatted version of number using the format string |
Math Functions
| Function | Description |
|---|---|
ceiling(number) |
Returns the smallest integer greater than or equal to the given number |
floor(number) |
Returns the largest integer less than or equal to the given number |
round(decimal) |
Returns the nearest integer to the given decimal |
sum(node-set) |
Returns the sum of the numeric values of each node in the set |
Node Functions
| Function | Description |
|---|---|
node() |
Selects all kinds of nodes |
text() |
Selects text nodes |
name() |
Returns the name of the current node |
position() |
Returns the position of the current node |
last() |
Returns the position of the last node in the context |
comment() |
Selects comment nodes |
processing-instruction() |
Selects processing instruction nodes |
XPath Selectors
Descendant Selectors
| CSS Equivalent | XPath | Description |
|---|---|---|
div |
//div |
Select all div elements |
div h1 |
//div//h1 |
Select all h1 within a div element |
ul > li |
//ul/li |
Select all li elements that are direct children of ul |
div > p > a |
/div/p/a |
Select all anchors within paragraph tags inside a div |
div > * |
//div/* |
Select all direct child elements of div |
:root |
/ |
Select the root element of the DOM |
:root > body |
/body |
Select the body tag |
Attribute Selectors
| CSS Equivalent | XPath | Description |
|---|---|---|
#id |
//*[@id="id"] |
Select elements with matching ID |
.class |
//*[@class="class"] |
Select elements with matching class |
a[rel] |
//a[@rel] |
Select all anchors with a rel attribute |
a[href^='/'] |
//a[starts-with(@href, '/')] |
Select anchors with href starting with '/' |
a[href$='.txt'] |
//a[ends-with(@href, '.txt')] |
Select anchors with href ending with '.txt' |
a[rel~='details'] |
//a[contains(@rel, 'details')] |
Select anchors whose rel value contains 'details' |
input[type="password"] |
//input[@type="password"] |
Select all password input fields |
a#btn[for="XYZ"] |
//a[@id="btn"][@for="XYZ"] |
Select anchor with id 'btn' and for attribute 'XYZ' |
Order Selectors
| CSS Equivalent | XPath | Description |
|---|---|---|
ul > li:first-of-type |
//ul/li[1] |
Select first li that is a child of ul |
ul > li:nth-of-type(2) |
//ul/li[2] |
Select second li that is a child of ul |
li#id:first-of-type |
//li[1][@id="id"] |
Select first li with a specific id value |
ul > li:last-of-type |
//ul/li[last()] |
Select last li that is a child of ul |
a:first-child |
//*[1][name()="a"] |
Select the first anchor element that is a first child |
a:last-child |
//*[last()][name()="a"] |
Select the last anchor element that is a last child |
Sibling Selectors
| CSS Equivalent | XPath | Description |
|---|---|---|
h1 ~ ul |
//h1/following-sibling::ul |
Select all ul tags that are following siblings of h1 |
h1 ~ #id |
//h1/following-sibling::[@id="id"] |
Select elements with a specific ID that are siblings of h1 |
h1 + ul |
//h1/following-sibling::ul[1] |
Select the first ul sibling immediately following h1 |
Contextual Selectors
| XPath | Description |
|---|---|
//img |
All image elements |
//img/*[1] |
First child of each image element |
//ul/child::li |
First child li of ul |
//img[1] |
First image element |
//img/*[last()] |
Last child of each image element |
//img[last()] |
Last image element |
//img[last()-1] |
Second-to-last image element |
//ul[*] |
ul elements that have any children |
Other Useful Locator Patterns
| XPath | Description |
|---|---|
//p[not(@id)] |
Select all paragraph tags without an id attribute |
//button[text()="Submit"] |
Select a button with exact text "Submit" |
//button[contains(text(),"pass")] |
Select a button whose text contains "pass" |
//product[@price > 3] |
Select product elements where price attribute is greater than 3 |
//ul[*] |
Select ul elements with any children |
//ul[li] |
Select ul elements that have li children specifically |
//a[@name or @href] |
Select anchors with a name or href attribute |
//a | //div |
Union of all anchor and div elements |
//table[count(tr) > 1] |
Select tables with more than one row |
//*[.="t"] |
Select elements containing exactly the text "t" |
//a[contains(text(), "Log Out")] |
Select anchors whose text contains "Log Out" |
//a[not(contains(text(), "Log Out"))] |
Select anchors whose text does not contain "Log Out" |
//a[not(@disabled)] |
Select all non-disabled anchor elements |
Attribute Selector Patterns
| XPath | Description |
|---|---|
//img[@id='myId'] |
Image element with id equal to 'myId' |
//img[@id!='myId'] |
Image elements with id not equal to 'myId' |
//img[@name] |
Image elements that have a name attribute |
//*[contains(@id, 'Id')] |
Any element where id contains the string 'Id' |
//*[starts-with(@id, 'Id')] |
Any element where id starts with 'Id' |
//*[ends-with(@id, 'Id')] |
Any element where id ends with 'Id' |
//*[matches(@id, 'r')] |
Any element where id matches the regex 'r' (XPath 2.0+) |
//*[@id='X' or @name='X'] |
Elements with id X or name X |
//*[@name="N"][@value="v"] |
Elements with name N and value v |
//*[@name="N" and not(@value="v")] |
Elements with name N but not value v |
//input[@type="submit"] |
Submit input buttons |
//section[//h1[@id='hi']] |
Returns section if it contains an h1 descendant with id 'hi' |
//*[@id="TestTable"]//tr[3]//td[2] |
Cell at row 3, column 2 of a table with id 'TestTable' |
//input[@checked] |
Checked checkboxes or radio buttons |
//a[@disabled] |
All disabled anchor elements |
Dynamic XPath Strategies
Dynamically generated IDs are one of the most common problems in real-world Selenium automation. When an element has an ID like input_abc123def that changes on every page load, you cannot use an exact attribute match. These patterns handle dynamic attributes reliably.
Partial Attribute Matching
| Pattern | Example | Use Case |
|---|---|---|
contains(@attr, 'value') |
//input[contains(@id, 'username')] |
ID contains a stable substring even if the full value changes |
starts-with(@attr, 'prefix') |
//div[starts-with(@id, 'modal')] |
ID always starts with the same prefix regardless of dynamic suffix |
ends-with(@attr, 'suffix') |
//input[ends-with(@name, '_field')] |
Attribute always ends with a known suffix |
| Combining conditions | //input[contains(@id, 'user') and @type='text'] |
Narrow down results when a partial match is not unique enough |
Text-Based Location
| Pattern | Example | Use Case |
|---|---|---|
| Exact text match | //button[text()='Log In'] |
Button or link with a known, stable label |
| Partial text match | //button[contains(text(),'Submit')] |
Label text may have surrounding whitespace or vary slightly |
| Normalized text | //label[normalize-space(text())='First Name'] |
Text node has inconsistent whitespace in the source |
Navigating from a Known Anchor
When a target element has no reliable attributes, navigate to it from a stable nearby element:
<!-- Locate a label, then find its associated input -->
//label[text()='Email']/following-sibling::input[1]
<!-- Locate a table row by cell text, then get another cell in the same row -->
//td[text()='John']/parent::tr/td[3]
<!-- Navigate from a section heading to its sibling content -->
//h3[text()='Shipping Address']/following-sibling::div[1]Using XPath in Selenium
Here is how to use XPath expressions in Selenium WebDriver across the two most common languages.
Python
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()
driver.get("https://example.com")
# Basic XPath lookup
element = driver.find_element(By.XPATH, "//input[@id='username']")
# Find multiple elements
items = driver.find_elements(By.XPATH, "//ul[@class='results']/li")
# Wait for element before interacting
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//button[text()='Submit']"))
)
element.click()Java
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
WebDriver driver = new ChromeDriver();
driver.get("https://example.com");
// Basic XPath lookup
WebElement element = driver.findElement(By.xpath("//input[@id='username']"));
// Find multiple elements
List<WebElement> items = driver.findElements(By.xpath("//ul[@class='results']/li"));
// Wait for element before interacting
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement btn = wait.until(
ExpectedConditions.presenceOfElementLocated(By.xpath("//button[text()='Submit']"))
);
btn.click();Testing XPath in Browser DevTools
You can validate any XPath expression directly in Chrome or Firefox DevTools without writing a single line of Selenium code.
Method 1: Elements Panel Search
- Open DevTools (F12)
- Go to the Elements tab
- Press Ctrl+F (Windows/Linux) or Cmd+F (Mac) to open the search bar
- Type your XPath expression directly into the search bar
- DevTools highlights all matching elements and shows a count
Method 2: Console with $x()
The $x() function in the browser console evaluates an XPath expression and returns all matching elements as an array:
// Run in the browser console (F12 > Console tab)
// Find all buttons with text "Submit"
$x("//button[text()='Submit']")
// Find input fields with dynamic IDs
$x("//input[contains(@id, 'user')]")
// Count matching elements
$x("//li[@class='active']").lengthMethod 3: Copy XPath from DevTools
- Open DevTools (F12)
- Click the element picker (top-left cursor icon in the DevTools panel)
- Click the element on the page
- Right-click the highlighted code in the Elements panel
- Select Copy > Copy XPath
Note: the copied XPath is usually an absolute path. Treat it as a starting point and rewrite it as a relative expression before adding it to your test scripts.
XPath vs. CSS Selectors
Both XPath and CSS selectors locate elements, but they have different strengths. Understanding the tradeoffs helps you pick the right tool for each situation.
| Feature | XPath | CSS Selector |
|---|---|---|
| Traverse up (parent) | Yes: //input/.. or parent::div |
No (CSS cannot traverse upward) |
| Select by text content | Yes: //a[text()='Home'] |
No |
| Partial attribute matching | Yes: contains(), starts-with() |
Yes: [attr*=val], [attr^=val] |
| Select by index | Yes: //li[3] |
Yes: li:nth-of-type(3) |
| Axis navigation | Full (ancestor, sibling, following, preceding) | Limited (next-sibling only) |
| Readability | Verbose, steeper learning curve | Concise, familiar to front-end developers |
| Performance in browsers | Slightly slower | Faster (browsers are optimized for CSS) |
| Dynamic ID handling | Excellent with contains() |
Good with attribute substring selectors |
| Shadow DOM | Limited support | Better support |
Use CSS selectors for straightforward element selection when you know the class, ID, or tag. Use XPath when you need to traverse upward, match by text, use complex axis relationships, or handle dynamic attributes that CSS substring selectors cannot accommodate as cleanly.
Common XPath Mistakes
These are the patterns most likely to cause brittle tests or unexpected failures in real Selenium projects.
1. Using Absolute XPath
Absolute XPath breaks the moment a developer adds or removes a wrapping element anywhere in the path. Always use relative XPath in test scripts.
<!-- Fragile: breaks if any ancestor changes -->
/html/body/div[2]/div[1]/form/input[1]
<!-- Robust: targets the element by its own attributes -->
//input[@name='email']2. Index-Based Locators Without a Reliable Anchor
XPath like //div[3] depends entirely on DOM order. Adding a feature that inserts a new div above your target breaks the locator silently.
<!-- Fragile: order-dependent -->
//div[3]
<!-- Robust: select by intent -->
//div[@id='checkout-summary']3. Whitespace Issues with text()
The text() function matches exact text node content. If the HTML has leading, trailing, or extra internal whitespace, an exact match fails.
<!-- Fails if label renders as " First Name " -->
//label[text()='First Name']
<!-- Robust: strips whitespace before matching -->
//label[normalize-space(text())='First Name']4. Matching Class Attributes Exactly
Using @class='btn' fails when the element has multiple classes like class="btn btn-primary".
<!-- Fails for multi-class elements -->
//button[@class='btn']
<!-- Robust: checks if the class list contains 'btn' -->
//button[contains(@class,'btn')]5. Case Sensitivity
XPath 1.0 (the version used in most browsers) is case-sensitive. The element <INPUT> does not match //input. HTML documents served in standard mode use lowercase tags, but XML documents may vary. Use translate() if you need case-insensitive matching.
//input[translate(@type,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='submit']XPath with jQuery
jQuery supports a subset of XPath-style expressions. The table below maps common jQuery traversal methods to their XPath equivalents.
| jQuery | XPath | Description |
|---|---|---|
$('ul > li').parent() |
//ul/li/.. |
Select all ul elements that are parents of li |
$('li').closest('section') |
//li/ancestor-or-self::section |
Select the nearest ancestor section of any li |
$('p').text() |
//p/text() |
Get text content within paragraph tags |
What to Learn Next
XPath gives you precision when simpler locators fall short. The patterns that matter most in day-to-day Selenium work are relative paths with contains() and starts-with() for dynamic attributes, axis navigation for reaching elements without direct identifiers, and text-based targeting with normalize-space() for labels and buttons.
Once you are comfortable with XPath, the logical next step is building maintainable locator strategies into a Page Object Model, which keeps your XPath expressions organized and isolated from test logic.
Browse the top-rated Selenium courses on hackr.io
Frequently Asked Questions
What is the difference between absolute and relative XPath in Selenium?
Absolute XPath starts from the document root (beginning with /html) and traces the complete path to the element. Relative XPath starts from anywhere in the document using // and locates an element based on its own attributes or relationship to nearby elements. Use relative XPath in test scripts. Absolute paths break whenever the DOM structure changes, even if the target element itself is unchanged.
When should I use XPath instead of CSS selectors?
Use XPath when you need to: traverse upward to a parent or ancestor element, locate elements by their visible text content, use complex axis navigation (following-sibling, preceding, ancestor), or combine multiple attribute conditions with text matching. Use CSS selectors for straightforward class, ID, or tag selection where performance is a priority, since browsers execute CSS selectors faster than XPath.
How do I find an element by its text using XPath?
Use the text() function for exact matches: //button[text()='Submit']. Use contains(text(), 'value') for partial matches: //a[contains(text(), 'Log In')]. When whitespace is unpredictable, wrap the text node with normalize-space(): //label[normalize-space(text())='Email Address'].
Why does my XPath break when the page layout changes?
The most common cause is using absolute XPath or index-based locators like //div[3]. Both depend on the exact position of elements in the DOM tree. Any layout change upstream of the target breaks the locator. Write relative XPath that targets the element by its own stable attributes (id, name, data attributes) rather than its position.
How do I handle dynamic element IDs with XPath?
Use contains() to match a stable substring within the dynamic ID: //input[contains(@id, 'username')]. If the ID always begins with a known prefix, use starts-with(): //div[starts-with(@id, 'modal-')]. For even more robust targeting, combine a partial attribute match with another stable attribute like type or name.
How do I test an XPath expression in Chrome DevTools without running Selenium?
Open DevTools (F12), go to the Console tab, and use the built-in $x() function. Type $x("//your/xpath/here") and press Enter. The console returns an array of all matching DOM elements. You can also press Ctrl+F in the Elements tab and type your XPath expression directly into the search field.
What does the following-sibling axis do in Selenium XPath?
The following-sibling axis selects all elements at the same level of the DOM tree that come after the context node. It is useful when you know a label or heading but need to target an element next to it. For example, //label[text()='Password']/following-sibling::input[1] finds the first input field that shares a parent with the Password label.
What is a real-world XPath example I can use in Selenium?
Imagine a login form. The email input has a dynamically generated ID like input_9f2a. You can locate it reliably with: //input[contains(@id, 'input') and @type='email']. Or navigate from the visible label: //label[text()='Email']/following-sibling::input[1]. Both approaches survive ID changes and minor DOM restructuring, making your tests less brittle over time.