Blog
Dynamic XPath in Selenium Explained With Practical Examples

Adwitiya Pandey
Published on

Table of contents
Lorem ipsum
Lorem ipsum
Lorem ipsum
Lorem ipsum
Lorem ipsum
Dynamic XPath lets Selenium find elements whose attributes keep changing. It works, but it comes at a cost. The expressions get complicated, they need constant attention, and every one of them is still a single guess about where an element lives. Functions like contains(), starts-with() and the XPath axes help you survive dynamic pages, but they're workarounds for a deeper problem: relying on one locator in an application that never stops changing.
This guide walks through the XPath techniques that actually hold up in Selenium, with working examples for each. It also covers where XPath runs out of road, and why more teams are moving to element identification that doesn't depend on a single expression.
What is Dynamic XPath in Selenium?
Dynamic XPath is an XPath expression that uses functions, conditions and partial matching to find elements whose attributes change. A static XPath depends on fixed values. A dynamic one targets the part of the element that stays the same, so it can cope with auto-generated IDs, shifting class names and unstable page structures.
The Dynamic Element Problem
Modern applications built with React, Vue or Angular often generate element attributes at runtime. Here's the same button in two different sessions:
A static XPath like this works once and then breaks:
Dynamic XPath targets the stable part of the ID instead:
Why XPath Matters in Selenium
XPath (XML Path Language) is the most powerful locator strategy in Selenium because it:
Move in any direction through the DOM, including up to parents and ancestors, across to siblings, and down to children and descendants
Combine several conditions with
and,orandnot()Find elements by their visible text when attributes are unreliable
Handle complex nested structures where simple CSS selectors struggle
Modern browsers now support the CSS :has() selector, which closes some of that gap. XPath is still the better fit for text matching and multi-step relationships, though. The trade-off is that without discipline, XPath expressions quickly become unreadable and fragile.
XPath Types: Absolute vs Relative
Absolute XPath
Absolute XPath spells out the full path from the root <html> element down to the target:
Problems:
Breaks when page structure changes
Verbose and unreadable
Slower execution as Selenium traverses entire path
Maintenance nightmare in dynamic applications
When to use: Never in production automation. Only for one-off debugging.
Relative XPath
Relative XPath starts with // and searches from anywhere in the document:
The benefits are:
It survives most structural changes around the element
It's readable, and describes the element by what it is
It's the standard for all maintainable automation scripts
Best practice: Always start with // and anchor the expression to something meaningful and stable.
Dynamic XPath Functions and Techniques
Using contains() for Partial Matches
contains() matches elements where an attribute includes a given substring. It's the go-to function for dynamic IDs and classes.
Syntax: contains(@attribute, 'value')
Example 1: Dynamic ID
This matches submit_btn_47392, submit_btn_85120 and any other variant.
Example 2: Dynamic Class with Multiple Values
Watch out here. contains(@class, 'alert') would also match alert-error, alert-success and no-alert. To match one whole class name exactly, pad the class list with spaces:
Example 3: Combining Multiple Contains Conditions
Caution: contains() can match more than you intended. Always check the expression returns exactly one element, either in the browser console with $x("your xpath") or with driver.findElements().
Using starts-with() for Predictable Prefixes
starts-with() matches elements where an attribute begins with a specific value.
Syntax: starts-with(@attribute, 'value')
Example 1: ID with Consistent Prefix
Example 2: Data Attributes
When to use it: When the dynamic part of the value always appears at the end. It's more precise than contains(), because it can't accidentally match the same text in the middle of a value.
Using text() for Content-Based Location
text() finds elements by their text content, which is often the most stable thing about an element because it's what users actually see.
Syntax: text()='exact text'
Example 1: Exact Text Match
Example 2: Partial text match
Example 3: Case-Sensitive Matching
XPath text matching is always case-sensitive:
Whitespace gotcha: text()='Login' won't match " Login " with surrounding spaces or line breaks, which templating frameworks often add. Use normalize-space() instead:
There's a second, subtler gotcha. text() only looks at the element's own text nodes. If the text sits inside a child element, like <button><span>Login</span></button>, then //button[text()='Login'] finds nothing. normalize-space() with no argument reads all the text inside the element, including children, so it handles both cases.
XPath Axes for Navigating Relationships
Axes locate elements by their relationship to other elements, rather than by their own attributes. This is especially useful when the target element has nothing stable about it, but something next to it does.
Following-Sibling Axis
This selects siblings that come after the reference element.
Syntax: following-sibling::tagname
Example 1: Select the input after a label
Example 2: Select the second button after a heading
Preceding-Sibling Axis
This selects siblings that come before the reference element.
Parent Axis
This moves up to the direct parent.
You can also write it as //input[@name='email']/..
Ancestor Axis
This selects any ancestor, such as the parent, grandparent and so on up to the root. It's very useful for tables:
A more practical version finds the Delete button in the row for a specific customer:
Child and Descendant Axes
child:: selects direct children only. descendant:: selects anything nested at any depth.
/ is shorthand for child::, and // is shorthand for descendant-or-self::.
Logical Operators: AND, OR, NOT
These combine conditions for more precise targeting.
and operator
or operator
not() function
Complex Combination
This finds the Edit button in any table row marked Active that isn't archived:
XPath Indexing for Multiple Matches
When an expression matches several elements, indexing picks one.
Syntax: (xpath)[index]. XPath indexing starts at 1, not 0.
Important: indexing inside and outside the brackets behaves differently.
This catches out a lot of people. Wrap the expression in brackets when you mean "the first match overall".
Advanced Dynamic XPath Patterns
Combining Multiple Functions
This finds the Buy button inside the pricing card titled "Pro Plan", even though the button's ID is generated:
Wildcard for Unknown Tag Names
* matches any element:
Caution: Wildcards (*) force Selenium to search the entire DOM, causing performance issues. Use specific tag names when possible.
Handling Dynamic Attributes with Multiple Conditions
When no single attribute is reliable, several partial signals together can pin the element down:
Case-Insensitive Text Matching
XPath 1.0, which is what browsers support, has no lower-case() function. The workaround is translate():
It works, but it's hard to read. If you only need to handle a couple of known variations, or is clearer:
XPath Optimization for Performance
Performance Best Practices
Prefer Specific Tag Names Over Wildcards
Use Stable Attributes
Test-specific and accessibility attributes change far less often than generated IDs or styling classes:
Minimize Axis Traversal
Every step through the tree is another thing that can break:
Avoid Complex Predicates
Deeply nested conditions are slow to evaluate and slower to understand. If an expression needs more than two or three conditions, look for a better anchor, or ask the developers to add a data-testid.
Readability vs Performance Trade-offs
In most real test suites, the performance difference between two reasonable XPath expressions is tiny compared with page load and wait times. Readability matters far more, because someone will have to fix the expression when it breaks.
Rule: Write for the next person who has to maintain the test, unless profiling shows a real bottleneck.
Testing Dynamic XPath in Real Environments
What Actually Differs Between Browsers
Selenium evaluates XPath using the browser's own XPath 1.0 engine, and that engine behaves the same way in Chrome, Edge, Firefox and Safari. When an XPath works in one browser and fails in another, the expression is rarely the cause. The DOM is.
The usual culprits are:
Different markup: Some sites serve different HTML to different browsers or devices.
Responsive layouts: On smaller viewports, menus collapse and elements get hidden or duplicated, so an expression can match nothing or match twice.
Shadow DOM: XPath can't look inside a shadow root. Selenium 4 lets you reach shadow content with
getShadowRoot(), but only CSS selectors work inside it.Iframes: Elements inside an iframe are invisible to XPath until you switch into the frame.
Here's how to handle the last two in Selenium 4:
Cross-Browser Validation Strategy
Run the same locators across every browser you support, ideally through Selenium Grid or a cloud device provider:
Anything other than exactly one match in any browser is worth investigating before it causes a flaky test.
Best Practices for Current Selenium XPath Users
If You Must Use XPath
Prefer Stable Attributes
Ask your developers to add data-testid attributes to key elements. It's the single most effective thing you can do to reduce locator maintenance.
Keep Expressions Simple
If you can't tell what an expression does at a glance, it's too complex. Break it up or find a better anchor.
Document Your XPath Strategy
Keep locators in one place, with names that say what the element is. The Page Object pattern does this well:
When a locator breaks, you fix it once instead of hunting through every test.
Implement Retry Logic
Dynamic frameworks often re-render elements, which causes StaleElementReferenceException. A FluentWait that ignores stale references retries automatically:
Use Explicit Waits
Never use Thread.sleep() to wait for dynamic elements. Wait for the specific condition you need:
Hybrid Approach: XPath + AI-Augmented Testing
Many enterprise teams don't switch overnight. A common pattern looks like this:
Keep existing Selenium XPath tests for the critical paths that are stable today
Build new tests on an AI-native platform
Gradually move the highest-maintenance XPath tests across
Measure the maintenance time saved, and expand from there
The Fundamental Limitations of XPath Approach
Single Locator Dependency
XPath expressions, no matter how sophisticated, rely on a single locator strategy. When the identified attribute changes, tests break. Dynamic XPath only delays the inevitable.
The Reality:
62% of testers use Selenium
Only 19.3% achieve over 50% automation coverage
80% of time spent on maintenance, 10% on authoring
XPath brittleness is a primary maintenance driver
Maintenance Burden at Scale
Consider an enterprise test suite with 1,000 test cases:
Average 10 XPath expressions per test = 10,000 XPath locators
If 5% of DOM changes per release (conservative) = 500 broken locators
Average 15 minutes to fix each locator = 125 hours maintenance per release
Monthly releases = 1,500 hours per year on XPath maintenance alone
This doesn't include time spent debugging why tests failed, validating fixes, or regression testing.
The Complexity Trap
As applications get more dynamic, expressions grow to keep up, until you end up with something like this:
Expressions like this are:
Hard to understand and risky to change
Fragile to small DOM changes anywhere along the chain
Slow to evaluate
Very hard to debug when they fail, because the error only tells you nothing matched
Framework Specific Challenges
React Applications: Components re-render constantly, generating new element references. Even stable XPath expressions can target stale elements.
Angular Applications: Two-way data binding causes frequent DOM updates. XPath must constantly re-evaluate after every interaction.
Vue Applications: Virtual DOM updates make XPath especially brittle. Elements may appear identical but have different internal references.
How AI-Augmented Element Identification Eliminates XPath Brittleness
Multiple Identifiers Instead of One Locator
AI-native test platforms don't bet everything on a single expression. They record many signals about each element, then use whichever ones still hold when the page changes.
A traditional Selenium approach depends on one path:
An AI-augmented approach stores a richer description of the same element. Conceptually, it looks something like this:
When the application changes, the platform still has several ways to find the element. If the ID changes, it can use the label, name and position. If the layout changes, it can use the label and surrounding text.
Self-Healing You Can Review
When an element changes, platforms like Virtuoso QA locate it using the other signals and update the test, instead of failing the run. The difference that matters at enterprise scale is visibility. Virtuoso QA logs every heal, showing what changed and why, so your team can accept or reject each fix. The suite keeps running, and nothing changes without a record. That record also feeds the release evidence for each run, so you can show what was tested and what was repaired.
A Natural Language Layer
AI-native platforms let you write tests in plain English, which removes raw XPath from the test entirely. Compare the two approaches.
The natural language equivalent:
This brings several benefits:
Tests read like business steps, not technical locators
Nobody needs XPath expertise to write or review them
When the DOM changes, the element identification updates while the test steps stay the same
Migration Path from XPath to AI-Augmented Testing
When to Migrate
High XPath Maintenance Burden: If more than 30% of test maintenance involves updating broken XPath expressions.
Complex Application Architectures: SPAs, progressive web apps, and microservices-based UIs with dynamic rendering.
Limited Automation Coverage: When XPath brittleness prevents scaling beyond 30-40% automation coverage.
Frequent Release Cycles: When weekly or daily releases mean constant XPath maintenance overhead.
Visit our Selenium migration page to see how Virtuoso QA supports seamless test migration while training your team to adopt AI-native testing effectively.
Technical Migration Approach
Modern AI native platforms offer agentic test generation that converts existing Selenium suites:
Step 1: Automated Script Analysis
AI analyzes existing Selenium scripts to understand test intent and element identification patterns.
Step 2: Natural Language Conversion
XPath-based interactions become plain English steps.
Before:
After:
Step 3: Comprehensive Element Modeling
AI builds multi-dimensional element models replacing brittle XPath expressions.
Step 4: Validation and Parallel Execution
Migrated tests run in parallel with existing Selenium tests to validate accuracy.
Conclusion: The Inevitable Evolution Beyond XPath
Dynamic XPath techniques like contains(), starts-with(), normalize-space() and axes genuinely help Selenium cope with changing elements. Every Selenium engineer should know them. But they're ways of surviving change, not removing its cost. As applications get more dynamic, the expressions get longer, and the maintenance grows with them.
The real problem isn't XPath syntax. It's depending on a single locator in an application that changes every sprint. Identifying elements from many signals, healing tests in a way your team can review, and writing tests in plain English all tackle that problem at the root. For teams spending more time fixing locators than adding coverage, that's where the effort is better spent.
Related Reads
Frequently Asked Questions
What is the difference between static XPath and dynamic XPath?
How do you write dynamic XPath for elements with changing IDs?
What is the syntax for contains() in XPath?
Should I use absolute or relative XPath in Selenium?
How do I locate elements by text content in XPath?
Should I migrate from Selenium XPath to AI native testing?











