Recent industry data shows the proportion of teams experiencing test flakiness grew from 10% in 2022 to 26% in 2025, alongside a 23% increase in pipeline complexity.
As modern frontend applications become more dynamic, even small UI or DOM changes can break unstable Selenium locators and create unreliable test behavior. XPath is often blamed for this instability, especially in applications built with React, Angular, and component-heavy frontend architectures.
In many cases, though, the issue is not XPath itself, but how the locator is written.
In this guide, you’ll learn how to write stable XPath locators in Selenium, handle dynamic elements, avoid brittle locator patterns, and use XPath effectively in modern web applications.
XPath Syntax: Attributes, Text, and Partial Matches in Selenium
XPath syntax defines how Selenium locates elements inside the DOM using tags, attributes, text values, and element relationships.
Most XPath locators in Selenium follow this structure:
//tagname[@attribute='value']
For example:
//input[@id='email']
This locator searches for an input element whose id attribute equals email.
XPath becomes useful when stable IDs or simple selectors are unavailable. Instead of depending only on one attribute, XPath can combine multiple conditions and relationships to identify elements more reliably.
Selecting Elements Using Attributes
Attributes are the most common way to write XPath locators in Selenium.
Example:
//button[@type='submit']
You can also combine multiple attributes:
//input[@type='text'][@name='username']
This reduces ambiguity when similar elements exist on the page.
However, modern frontend applications often generate dynamic attributes during runtime. In React or component-based applications, IDs and class names may change between builds or sessions.
In these cases, exact attribute matching becomes brittle.
For example:
//button[@class='btn-1a2b3c']
This locator may fail if the generated class changes after deployment.
A partial match is usually more stable.
Using Partial Matches with contains()
The contains() function helps locate elements when part of an attribute remains stable.
Example:
//button[contains(@class,'btn')]
This locator still works even if the full class value changes dynamically.
contains() is commonly used for:
- dynamic class names
- generated IDs
- reusable UI components
- partially changing attributes
But it should be used carefully.
Overly broad partial matches can unintentionally select multiple elements and create unstable tests.
For example:
//div[contains(@class,'container')]
may match several unrelated elements on large pages.
Experienced automation engineers usually anchor partial matches to more stable surrounding relationships or combine them with additional conditions.
Using starts-with() for Dynamic Attributes
Some frontend systems generate IDs with predictable prefixes.
Example:
<input id="user_48392" />
Instead of matching the entire value:
//input[@id='user_48392']
you can use:
//input[starts-with(@id,'user_')]
This makes the locator more resilient to changing numeric suffixes.
Selecting Elements Using Visible Text
XPath can also locate elements using visible text content.
Example:
//button[text()='Login']
This is useful for:
- buttons
- links
- menu items
- validation messages
However, exact text matching can become fragile in applications with:
- localization
- dynamic spacing
- changing labels
For example, this may fail because of extra whitespace:
//button[text()='Sign In']
A more stable approach is:
//button[contains(text(),'Sign In')]
Or:
//button[normalize-space()='Sign In']
normalize-space() removes unnecessary whitespace and makes text matching more reliable in dynamic UIs.
Which XPath Strategy Should You Use?
Different UI structures require different XPath strategies. The goal is to choose the locator approach that remains stable as the frontend changes.
| Situation | Recommended XPath Strategy | Avoid |
|---|---|---|
| Stable data-testid exists | //button[@data-testid=’checkout’] | Text-based XPath |
| Dynamic React or generated classes | contains() or relationship-based XPath | Exact class match |
| Repeated cards or components | Scope XPath to parent container | Global XPath |
| Forms with labels | following-sibling axis | Positional indexes |
| Localized or changing UI text | Semantic attributes | Exact text matching |
| Dynamic IDs with stable prefixes | starts-with() | Full ID match |
| Dynamic tables and grids | ancestor + descendant axes | Deep absolute XPath |
How to Write XPath Locators in Selenium
Writing XPath locators in Selenium is not about creating the longest or most specific path to an element. It is about choosing the most stable way to identify that element in the DOM.
A good XPath locator should answer two questions:
- What uniquely identifies this element?
- Will that identifier remain stable after small UI changes?
Follow this process.
Step 1: Inspect the Element and Its Surrounding DOM
Open the page in the browser, right-click the element, and inspect it.
Do not copy the full XPath from DevTools and use it directly in your test. Browser-generated XPath often depends on the exact DOM hierarchy, which makes it fragile.
Look for stable signals such as:
- id
- name
- type
- aria-label
- data-testid
- visible text
- nearby labels
- parent containers
The goal is to understand the element’s context before writing the locator.
Step 2: Choose the Most Stable Anchor
Next, decide what the XPath should depend on.
Use this order of preference:
- Stable test-specific attributes, such as data-testid
- Stable semantic attributes, such as name, type, or aria-label
- Visible text, when the label is unlikely to change
- Nearby labels or parent containers, when direct attributes are dynamic
- Index-based selection only as a last option
This step matters because most flaky XPath locators fail by depending on unstable implementation details, such as generated class names, layout wrappers, or element position.
Step 3: Write the Simplest XPath That Identifies the Element
Start with the simplest reliable XPath.
For example:
//input[@name='email']
or:
//button[@type='submit']
Simple locators are easier to read, debug, and maintain.
Do not make the XPath more complex unless the page requires it.
Step 4: Add Conditions Only When the Match Is Ambiguous
If the XPath matches more than one element, add another stable condition.
For example:
//input[@type='text'][@name='username']
This is better than using an index like:
(//input[@type='text'])[2]
The index-based locator depends on element order. If another text input is added before it, Selenium may interact with the wrong field.
Step 5: Use Text When the Visible Label Is Stable
For buttons, links, tabs, menu items, and alerts, visible text can be a strong locator anchor.
Example:
//button[normalize-space()='Login']
normalize-space() is usually safer than exact text() matching because it handles extra spaces and line breaks in the DOM.
Use text-based XPath carefully in applications with frequent copy changes or localization. If the UI text changes often, prefer stable attributes or relationships instead.
Step 6: Use DOM Relationships When Direct Attributes Are Weak
If the element does not have stable attributes, locate it through a nearby stable element.
For example, instead of:
/html/body/div[2]/form/div[3]/input
use:
//label[normalize-space()='Email']/following-sibling::input
This is more resilient because it depends on the relationship between the label and the input, not the exact page structure.
Relationship-based XPath is especially useful for forms, tables, reusable components, and enterprise dashboards where direct attributes are missing or dynamic.
Step 7: Scope the XPath to the Correct Section
If the same element appears multiple times, scope the XPath to a parent container.
For example:
//form[@id='loginForm']//input[@name='email']
or:
//div[@data-testid='user-card']//button[normalize-space()='Edit']
Scoping prevents Selenium from selecting the wrong matching element elsewhere on the page. This is important in component-based applications where the same button, input, or card pattern appears multiple times.
Step 8: Validate the XPath Before Using It in Selenium
Before adding an XPath to your Selenium test script, validate it in browser DevTools.
You can test XPath directly in Chrome DevTools by opening the Elements panel and pressing Ctrl + F or Cmd + F. Enter the XPath in the search box and check which element is highlighted.
A reliable XPath should pass these checks:
- It matches the intended element
- It does not match unrelated duplicate elements
- It still works after a page refresh
- It does not depend on generated classes or unstable indexes
- Another tester can understand it later
For example, if the page has an email input field, this XPath is usually easier to validate and maintain:
//input[@name='email']
But this XPath is more fragile:
/html/body/div[2]/form/div[3]/input
The first XPath identifies the field by its purpose. The second XPath depends on the exact DOM position. If a developer adds a wrapper div, banner, or layout section, the second XPath may break even if the email field still exists.
A locator that works once is not enough. It should be stable, readable, and specific enough for long-term test maintenance.
To make the next Selenium example easier to understand, consider this simple login form:
<form id="loginForm"> <label>Email</label> <input type="text" name="email" /> <label>Password</label> <input type="password" name="password" /> <button type="submit">Login</button> </form>
In this form, the email field can be located using its name attribute:
//input[@name='email']
The login button can be located using its visible text:
//button[normalize-space()='Login']
Step 9: Use the XPath in Selenium Code
Once the XPath is validated in DevTools, use it with By.xpath() in Selenium.
The example below opens a sample login page, finds the email field with XPath, enters an email address, finds the login button, and clicks it.
Save the sample HTML from the previous step as login.html, then replace the file path in the code with the correct path on your system.
Java example:
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
public class XPathLoginExample {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
try {
driver.get("file:///path/to/login.html");
WebElement emailInput = driver.findElement(By.xpath("//input[@name='email']"));
emailInput.sendKeys("user@example.com");
WebElement loginButton = driver.findElement(By.xpath("//button[normalize-space()='Login']"));
loginButton.click();
} finally {
driver.quit();
}
}
}Python example:
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
try:
driver.get("file:///path/to/login.html")
email_input = driver.find_element(By.XPATH, "//input[@name='email']")
email_input.send_keys("user@example.com")
login_button = driver.find_element(By.XPATH, "//button[normalize-space()='Login']")
login_button.click()
finally:
driver.quit()In both examples, Selenium first loads the page in the browser. Then it searches the DOM using the XPath expression.
This line finds the email input:
//input[@name='email']
This line finds the login button:
//button[normalize-space()='Login']
The important point is that XPath needs a browser page and a DOM to work against. Without that context, an XPath expression is only a locator string. Selenium can execute it only after the browser loads a page that contains matching elements.
Relative vs Absolute XPath in Selenium
XPath locators in Selenium are generally written in two ways:
- Absolute XPath
- Relative XPath
The difference is not just syntax. It directly affects how stable and maintainable the locator will be as the application changes.
What Is Absolute XPath?
Absolute XPath starts from the root of the DOM and follows the complete hierarchy to the target element.
Example:
/html/body/div[2]/div[1]/form/input
This XPath tells Selenium to follow the exact DOM path from the top of the page to the input field.
Absolute XPath is usually easy to generate because browser DevTools can copy it automatically. However, this convenience creates one of the most common Selenium anti-patterns.
A small frontend change can break the locator immediately.
For example:
- adding a wrapper <div>
- moving a form section
- inserting a banner
- restructuring layout containers
can change the DOM hierarchy and invalidate the XPath, even though the actual element still exists and behaves correctly.
This problem becomes much more common in modern frontend applications built with:
- React
- Angular
- Vue
- component-based UI libraries
because these frameworks frequently update layout structures during UI refactors.
That is why deeply nested absolute XPath locators are considered brittle in large Selenium test suites.
What Is Relative XPath?
Relative XPath starts from anywhere in the DOM instead of beginning at the root.
Example:
//input[@name='email']
or:
//label[normalize-space()='Email']/following-sibling::input
Relative XPath focuses on stable identifying information instead of the exact page hierarchy.
This makes the locator more resilient to frontend changes because Selenium does not depend on every intermediate container remaining unchanged.
For example, if additional layout wrappers are introduced, the following XPath usually continues working:
//button[normalize-space()='Checkout']
because it targets the button itself rather than its position inside the DOM tree.
Why Relative XPath Is Preferred in Selenium
Most Selenium frameworks prefer relative XPath because modern frontend applications change frequently.
Small UI updates, reusable components, or additional wrapper elements can easily break absolute XPath locators that depend on the exact DOM hierarchy.
Relative XPath is generally more stable because it relies on identifiable attributes, visible text, or element relationships instead of exact node position.
| Absolute XPath | Relative XPath |
|---|---|
| Depends on exact DOM hierarchy | Depends on stable identifying information |
| Breaks easily after layout changes | More resilient to UI updates |
| Harder to read and maintain | Easier to debug and maintain |
| Usually generated automatically by DevTools | Usually written manually for stability |
Example:
Absolute XPath:
/html/body/div[3]/div[2]/form/div/input
Relative XPath:
//form[@id='loginForm']//input[@name='email']
The second locator is easier to understand and less likely to fail after frontend changes.
Absolute XPath can still help during debugging or DOM inspection, but relative XPath is generally preferred for scalable Selenium automation frameworks.
Locating Elements by DOM Relationships Using XPath Axes in Selenium
XPath axes help Selenium locate elements based on their relationship with other elements in the DOM.
This becomes useful when:
- direct attributes are missing
- IDs are dynamic
- multiple similar elements exist
- the surrounding UI structure is more stable than the element itself
Instead of locating elements only by attributes or text, XPath axes allow Selenium to navigate through:
- parent elements
- child elements
- sibling elements
- ancestor containers
This is one of the biggest advantages XPath has over CSS selectors.
Using following-sibling to Locate Related Elements
following-sibling is commonly used when an element appears next to a stable label or heading.
For example:
<label>Email</label> <input type="text" />
XPath:
//label[normalize-space()='Email']/following-sibling::input
This locator identifies the input field relative to the label instead of depending on generated attributes or nested layout containers.
Relationship-based locators like this are usually more stable in dynamic forms and reusable UI components.
Using ancestor to Scope Elements to a Specific Container
Large applications often contain repeated elements such as multiple buttons named “Edit” or “Save”.
The ancestor axis helps scope the element to the correct section of the page.
Example:
//button[normalize-space()='Edit']/ancestor::div[@class='user-card']
This allows Selenium to identify the correct container associated with the button.
Axes-based scoping becomes especially useful in:
- dashboards
- reusable cards
- nested forms
- component-heavy applications
where identical elements appear multiple times.
Using parent to Navigate Up the DOM
The parent axis helps locate the immediate parent of an element.
Example:
//input[@name='email']/parent::div
This is useful when:
- validation messages appear inside parent containers
- styling classes exist on wrappers instead of inputs
- related elements are grouped together
Using descendant for Nested Elements
The descendant axis helps locate elements inside a specific container.
Example:
//form[@id='checkoutForm']/descendant::button[@type='submit']
This reduces ambiguity by restricting the search to a specific section of the page.
It is commonly used in:
- forms
- tables
- modals
- nested component structures
Handling Dynamic Elements and Flaky Locators with XPath in Selenium
Modern web applications rarely keep the DOM completely stable.
Frontend frameworks like React, Angular, and Vue frequently rerender components, generate dynamic attributes, and update parts of the UI without fully reloading the page. As a result, XPath locators that depend on unstable implementation details can quickly become flaky.
This is one of the biggest reasons Selenium tests fail after small frontend changes.
Most flaky XPath locators fail because they depend on:
- generated class names
- deeply nested layout wrappers
- dynamic IDs
- positional indexes
- changing text values
For example:
//div[3]/div[2]/button
This locator assumes the DOM structure will always remain identical.
If a new wrapper container or banner is added, Selenium may:
- fail to find the element
- target the wrong element
- interact with an outdated node
Modern frontend applications make this problem more common because components are frequently reused and rearranged during UI updates.
1. Avoid Dynamic Class Names and Generated IDs
Many frontend frameworks generate class names automatically during runtime or build compilation.
For example:
<button class="btn-13af7x primary-91ksl">
An XPath like:
//button[@class='btn-13af7x primary-91ksl']
is highly brittle because the class value may change after deployment.
Instead, prefer:
- stable attributes
- visible text
- parent-child relationships
- nearby labels
- data-testid attributes
A more stable locator would be:
//button[normalize-space()='Checkout']
or:
//div[@data-testid='checkout-section']//button
2. Use Relationship-Based XPath for Dynamic UIs
When direct attributes are unstable, DOM relationships are often more reliable than exact structure.
For example:
//label[normalize-space()='Email']/following-sibling::input
This locator depends on the relationship between the label and input field instead of layout depth or generated wrappers.
Relationship-based XPath is especially useful in:
- dynamic forms
- reusable UI components
- enterprise dashboards
- nested frontend layouts
because functional relationships usually change less frequently than implementation details.
3. Be Careful with Text-Based XPath
Text-based XPath can improve readability, but it may become unstable in applications with:
- localization
- A/B testing
- changing button labels
- dynamic UI messaging
For example:
//button[text()='Submit']
may fail if the UI changes the label to:
- “Continue”
- “Save”
- “Submit Order”
In these cases, combine text with stable containers or attributes instead of relying entirely on visible labels.
4. Reduce Locator Coupling to the DOM Structure
A stable XPath locator should identify the element semantically, not structurally.
Weak locator:
/html/body/div[2]/div[4]/form/div/input
Stronger locator:
//form[@id='signupForm']//input[@name='email']
The second locator depends on:
- form identity
- field purpose
instead of:
- exact nesting depth
- wrapper hierarchy
- page layout structure
This makes the locator easier to maintain as the frontend evolves.
5. Validate XPath Stability Before Adding It to the Test Suite
Before finalizing a locator, ask:
- Will this still work if a wrapper <div> is added?
- Does this depend on generated values?
- Would a small UI refactor break it?
- Is the locator tied to layout instead of meaning?
- Could another engineer understand this XPath quickly?
These checks help reduce flaky Selenium tests over time.
Experienced automation engineers usually spend more time thinking about locator stability than writing the XPath syntax itself. In large Selenium frameworks, stable locators directly affect debugging effort, CI reliability, and long-term maintenance cost.
XPath vs CSS Selectors in Selenium
XPath and CSS selectors are the two most commonly used locator strategies in Selenium.
Both can locate elements effectively, but they solve different problems and behave differently in modern web applications.
The goal is not to choose one universally. It is to use the locator strategy that creates the most stable and maintainable tests.
| XPath | CSS Selectors |
|---|---|
| Can navigate parent-child and sibling relationships | Cannot navigate upward in the DOM |
| Supports text-based matching | Does not support direct text matching |
| Better for relationship-based locators | Better for simple attribute matching |
| More flexible for complex DOM traversal | Usually shorter and easier to read |
| Commonly used for dynamic forms and tables | Commonly used for stable UI components |
When XPath Works Better
XPath is usually more effective when the element needs to be located relative to another element.
For example:
//label[normalize-space()='Email']/following-sibling::input
This type of relationship-based locator is difficult to express cleanly using CSS selectors.
XPath is also useful for:
- dynamic tables
- nested forms
- reusable components
- locating elements by visible text
- traversing complex DOM relationships
This flexibility is one of the main reasons XPath remains widely used in Selenium automation.
When CSS Selectors Work Better
CSS selectors are usually simpler when stable attributes already exist.
Example:
input[name='email']
For straightforward attribute matching, CSS selectors are often:
- shorter
- easier to read
- easier to debug
Modern frontend applications that expose stable attributes such as:
- data-testid
- id
- name
often work very well with CSS selectors.
When NOT to Use XPath in Selenium
XPath is powerful, but it is not always the best locator strategy in Selenium.
In many cases, XPath adds unnecessary complexity when a simpler and more stable locator already exists. Overusing XPath can also make tests harder to debug and maintain over time.
Avoid using XPath in these situations:
| Situation | Better Alternative | Why |
|---|---|---|
| Stable id or data-testid is available | CSS selector or ID locator | Simpler and easier to maintain |
| Simple attribute-based selection | CSS selectors | Cleaner and more readable |
| Shadow DOM elements | Shadow DOM APIs or framework-specific locators | Standard XPath cannot cross Shadow DOM boundaries |
| Frequently changing UI text | Semantic attributes | Text-based XPath becomes brittle |
| Generated frontend classes | Stable test attributes | Dynamic classes change across builds |
| Deeply nested layouts | Relationship-based locators | Structure-dependent XPath breaks easily |
| Highly dynamic component trees | Scoped locators with stable anchors | Broad XPath becomes unreliable |
Common XPath Mistakes That Break Selenium Tests
Most XPath-related Selenium failures are not caused by XPath itself. They happen because the locator depends on unstable implementation details that change as the frontend evolves.
These mistakes may work temporarily during local testing, but they often become flaky in large automation suites where UI changes happen frequently.
1. Using Deeply Nested Absolute XPath
One of the most common mistakes is relying on the full DOM hierarchy.
Example:
/html/body/div[2]/div[4]/form/div/input
This locator depends on the exact page structure remaining unchanged.
If a frontend developer adds:
- a wrapper <div>
- a new banner
- a layout container
- a responsive UI adjustment
the XPath may fail immediately.
Modern frontend frameworks make this especially risky because component structures change frequently during UI refactors.
A more stable approach is:
//form[@id='signupForm']//input[@name='email']
This identifies the element using stable meaning instead of DOM depth.
2. Depending on Dynamic Class Names
Many frontend frameworks generate class names automatically during runtime or build compilation.
Example:
<button class="btn-13af7x primary-91ksl">
Using:
//button[@class='btn-13af7x primary-91ksl']
creates a brittle locator because the class value may change after deployment.
This problem is common in:
- React applications
- CSS-in-JS systems
- Tailwind-heavy UIs
- component libraries
Instead, prefer:
- stable attributes
- visible text
- parent-child relationships
- data-testid
- semantic attributes
3. Overusing Index-Based XPath
Index-based locators are another major source of flaky tests.
Example:
(//button[@type='submit'])[2]
This assumes the second matching button will always remain in the same position.
If another button is inserted earlier in the DOM, Selenium may:
- click the wrong element
- fail the test
- interact with a hidden component
Indexes should usually be treated as a last resort, not a primary locator strategy.
4. Copy-Pasting XPath from Browser DevTools
Browser-generated XPath is often optimized for uniqueness, not stability.
For example, DevTools may generate locators containing:
- nested containers
- indexes
- temporary attributes
- long DOM paths
These locators usually work initially but become fragile after frontend changes.
Experienced automation engineers rarely use copied XPath directly in production frameworks. They typically rewrite it into:
- shorter
- relationship-based
- attribute-focused
- maintainable locators
5. Using Broad contains() Matches
contains() is useful for handling dynamic attributes, but overly broad matches can create unstable tests.
Weak locator:
//div[contains(@class,'container')]
This may unintentionally match multiple unrelated elements.
A more stable approach combines partial matches with additional context:
//div[@data-testid='checkout-section']//button[contains(@class,'primary')]
The locator becomes more predictable because it is scoped to a meaningful section of the page.
6. Ignoring Locator Readability
A technically valid XPath can still become difficult to maintain.
For example:
//div[@id='a1']//div[2]//span[contains(text(),'Submit')]
may work, but it does not clearly communicate intent.
Readable locators are easier to:
- debug
- review
- update
- maintain across teams
In large Selenium frameworks, locator readability directly affects long-term maintenance effort.
7. Treating XPath as Static Instead of Evolutionary
Frontend applications change continuously.
A locator that is stable today may become brittle after:
- component refactoring
- responsive layout updates
- localization changes
- UI redesigns
Experienced automation engineers regularly review and refactor XPath locators instead of assuming they will remain stable forever.
The most reliable Selenium frameworks treat locator maintenance as part of the automation lifecycle, not a one-time implementation task.
Conclusion
XPath remains one of the most effective Selenium locator strategies for handling dynamic elements, nested components, and relationship-based UI structures. While XPath is often considered fragile, most flaky locators fail because they depend on unstable DOM structure, generated classes, or positional indexes instead of stable UI meaning.
In modern Selenium frameworks, stable XPath locators are usually built around semantic attributes, visible text, and meaningful element relationships. The goal is not to write the shortest XPath possible, but to create locators that remain readable, maintainable, and resilient as frontend applications evolve.




