Selenium Locators and XPath
Locators tell Selenium which element to interact with. They're the foundation of every test — and the most common cause of failures when they're written badly. This guide covers all eight Selenium locators, CSS selector syntax, XPath functions and axes, dynamic elements and the order to choose them in, with examples tested against a real page.
The Sample Page
All examples below use this HTML. Note the dynamic IDs such as user-7f3a — in real applications they often change on every build.
<form id="loginForm">
<label for="user-7f3a">Username</label>
<input id="user-7f3a" name="username" class="form-control input-lg" type="text" placeholder="Enter username">
<label for="pass-9c1d">Password</label>
<input id="pass-9c1d" name="password" class="form-control" type="password">
<input id="remember" name="remember" type="checkbox">
<label for="remember">Remember me</label>
<button type="submit" class="btn btn-primary" data-testid="login-btn">Log in</button>
<a href="/forgot-password">Forgot password?</a>
</form>
<table id="users">
<tr>
<th>Name</th>
<th>Role</th>
<th>Action</th>
</tr>
<tr>
<td>Asha</td>
<td>Admin</td>
<td><input type="checkbox"> <button>Edit</button></td>
</tr>
<tr>
<td>Ravi</td>
<td>Tester</td>
<td><input type="checkbox"> <button>Edit</button></td>
</tr>
</table>
What Are Locators?
A locator is a strategy for finding an element in the DOM. You pass it to findElement() — which returns the first match or throws NoSuchElementException — or findElements(), which returns a list and an empty list if nothing matches.
Locators are created with the By class — an abstract class with a static method for each strategy.
The 8 Selenium Locators
| Locator | Example on the sample page | Notes |
|---|---|---|
By.id() |
By.id("remember") |
Best when the ID is unique and stable |
By.name() |
By.name("username") |
Great for form fields |
By.className() |
By.className("btn-primary") |
One class only — "btn btn-primary" throws an error |
By.tagName() |
By.tagName("input") |
Mostly for counting or collecting elements |
By.linkText() |
By.linkText("Forgot password?") |
Exact visible text of a link |
By.partialLinkText() |
By.partialLinkText("Forgot") |
Part of the link text |
By.cssSelector() |
By.cssSelector("[data-testid='login-btn']") |
Fast, readable, very flexible |
By.xpath() |
By.xpath("//button[text()='Log in']") |
Can match text and navigate in any direction |
The first six locate elements by a single attribute or text; CSS selectors and XPath combine attributes, structure and, for XPath, text.
CSS Selectors
| Pattern | Example | Matches |
|---|---|---|
tag#id |
input#remember |
The checkbox |
tag.class |
button.btn.btn-primary |
The login button |
[attribute='value'] |
input[name='password'] |
Password field |
| Test attribute ⭐ | [data-testid='login-btn'] |
Login button |
Starts with ^= |
input[id^='user-'] |
Username with dynamic ID |
Ends with $= |
input[name$='word'] |
Password field |
Contains *= |
a[href*='forgot'] |
Forgot-password link |
Direct child > |
form#loginForm > button |
Login button |
| Descendant (space) | #loginForm input[type='text'] |
Username |
:nth-child() |
table#users tr:nth-child(3) td:nth-child(2) |
"Tester" |
:not() |
#loginForm input:not([type='checkbox']) |
Username and password |
Important: CSS has no way to match an element by its text. The :contains("text") selector you'll see in older tutorials came from jQuery and never worked in Selenium.
Use XPath text() when you need text matching.
Modern browsers do support :has() for selecting a parent by its content, for example:
form:has([data-testid='login-btn'])
What Is XPath?
XPath, or XML Path Language, describes a path through the DOM. Browsers implement XPath 1.0, which Selenium uses.
It can locate elements by:
- Attributes
- Text
- Position
- Parent and child relationships
- Sibling relationships
- Other DOM relationships
Absolute vs Relative XPath
-
Absolute XPath starts at the root with a single slash:
/html/body/form/input[1]
It breaks as soon as any wrapper element changes.
-
Relative XPath starts with
//and anchors to something meaningful:
//input[@name='username']
Always use relative XPath in real projects — for maintainability, not speed. The performance difference is negligible.
More: Absolute vs Relative XPath.
XPath Functions and Operators
| Need | XPath |
|---|---|
| Attribute | //input[@name='username'] |
| Exact text | //button[text()='Log in'] |
| Text with stray spaces | //button[normalize-space()='Log in'] |
| Partial text | //a[contains(text(),'Forgot')] |
| Partial attribute | //input[contains(@class,'input-lg')] |
| Starts with dynamic ID | //input[starts-with(@id,'user-')] |
| AND | //input[@type='password' and @name='password'] |
| OR | //input[@name='username' or @name='email'] |
| Nth match | (//button[text()='Edit'])[2] |
XPath contains()
contains() is useful when part of an attribute or text is stable while the rest changes.
For example:
//input[contains(@class,'input-lg')]
This can match an input whose class contains input-lg.
You can also use it for visible text:
//a[contains(text(),'Forgot')]
XPath normalize-space()
Web pages sometimes contain unexpected spaces or line breaks around visible text. normalize-space() removes unnecessary whitespace before comparing the value.
//button[normalize-space()='Log in']
This is often more reliable than an exact text() match when whitespace is inconsistent.
No ends-with() in Browser XPath
There is no ends-with() function in XPath 1.0. It is an XPath 2.0 function, while browsers support XPath 1.0.
Use CSS $= instead when possible:
input[id$='-name']
Or use this XPath 1.0 workaround:
//input[
substring(@id, string-length(@id) - string-length('-name') + 1) = '-name'
]
XPath Axes
Axes move from a known element to a related one. They're what make locators work when the target itself has nothing stable to match.
The syntax is:
axis::node
For example:
following-sibling::input
Here, :: separates the axis from the node test.
| Axis | Finds | Example on the sample page |
|---|---|---|
parent:: |
The direct parent | //button[text()='Log in']/parent::form |
child:: or / |
Direct children | //form/child::button |
ancestor:: |
Parent, grandparent and above | //input[@name='username']/ancestor::form |
descendant:: or // |
Children, grandchildren and below | //table[@id='users']/descendant::button |
following-sibling:: |
Later siblings with the same parent | //td[text()='Ravi']/following-sibling::td[1] → "Tester" |
preceding-sibling:: |
Earlier siblings | //label[text()='Remember me']/preceding-sibling::input[1] |
following:: |
Anything after it in the document | //label[text()='Password']/following::input[1] |
preceding:: |
Anything before it | //button[text()='Log in']/preceding::input[@type='text'] |
self:: |
The node itself | Used inside conditions |
Easy Family Analogy
Think of XPath axes like a family tree:
- parent = your parent
- ancestor = parents and grandparents
- child = your children
- descendant = children and grandchildren
- following-sibling = younger brothers and sisters
- preceding-sibling = older brothers and sisters
The Most Useful Pattern — Dynamic Tables
Dynamic tables are one of the best use cases for XPath axes.
First, find the row using a value you know, then move to the cell or button you need.
//table[@id='users']//tr[td[text()='Asha']]/td[2]
Result:
Admin
Another example:
//td[text()='Ravi']/parent::tr//button
This locates Ravi's Edit button.
Watch the Slash
This is correct:
//div/following-sibling::div
But this:
//div//following-sibling::div
searches the siblings of every descendant instead.
This is a common XPath mistake and can result in unexpected matches.
Selenium 4 Relative Locators
Selenium 4 can also find elements by their visual position relative to another element.
For example:
import static org.openqa.selenium.support.locators.RelativeLocator.with;
WebElement password = driver.findElement(
with(By.tagName("input")).below(By.name("username")));
Available relationships include:
abovebelowtoLeftOftoRightOfnear
Relative locators can be useful when the visual relationship between elements is stable. However, they depend on page layout, so attribute- or structure-based locators should generally be preferred when they are available.
Locating Dynamic Elements
Dynamic elements are extremely common in modern applications. IDs may contain generated values, classes may change, and elements may be rendered only after an API call.
Use these approaches:
- Match the stable part — use
starts-with()orcontains()in XPath, or^=,*=and$=in CSS. - Use a different stable attribute — look for
name,data-testid,placeholder,type, or another predictable attribute. - Anchor to something stable nearby — use a label, heading or table value and navigate with XPath axes.
- Wait for the element — dynamic content may also require explicit waits.
See Selenium Waits.
Handling Duplicate IDs and Multiple Matches
Duplicate IDs are invalid HTML but happen in real applications. Don't blindly rely on By.id() when IDs are duplicated.
Instead:
- Scope the locator to a container:
#loginForm input[name='username']
- Use context from a nearby label, parent or sibling.
- As a last resort, use an index:
(//input[@id='duplicateId'])[2]
However, indexes can break when the page changes, so they should be used carefully.
Which Locator Should You Choose?
A practical locator preference is:
- ID — when it is unique and not auto-generated.
- Test attribute such as
data-testid— ask developers to add stable test attributes where appropriate. - Name — especially useful for form fields.
- CSS selector — useful for attributes, classes and structure.
- Relative XPath — use when you need text matching or axes.
- Never use absolute XPath for maintainable automation, and avoid blindly copying XPath from DevTools because it is often index-based and brittle.
Test every locator in DevTools first using the Elements panel and Ctrl+F.
The goal should generally be to get exactly one match.
You can also practise using the CSS & XPath Selector Playground.
From Real Projects
Identifying elements by id and class name and writing XPath was a daily part of my automation work on Canolog and Testsigma. I preferred id and class name when they were unique and stable, and used XPath for everything else — always keeping locators inside POM classes so they could be reviewed and fixed in one place. Prefer short relative XPath based on stable attributes over long absolute paths that break with every layout change.
FAQs
What are locators in Selenium?
Locators are strategies for finding elements on a web page. Selenium provides eight primary locator strategies through the By class:
- ID
- Name
- Class Name
- Tag Name
- Link Text
- Partial Link Text
- CSS Selector
- XPath
What is the By class?
By is an abstract Selenium class whose static methods create locator objects.
Examples include:
By.id("username");
By.name("password");
By.cssSelector("[data-testid='login-btn']");
By.xpath("//button[text()='Log in']");
These locators are then passed to methods such as findElement() and findElements().
Can CSS selectors find elements by text?
No. Standard CSS selectors do not provide a text selector equivalent to XPath's text().
The :contains() syntax found in some older tutorials came from jQuery and does not work as a standard Selenium CSS selector.
Use XPath instead:
//button[text()='Log in']
or:
//button[contains(text(),'Log')]
Which is better: absolute or relative XPath?
Relative XPath is generally preferable for maintainable automation because it can anchor to meaningful attributes, text or relationships rather than depending on the complete DOM hierarchy.
For example:
//input[@name='username']
is generally more maintainable than:
/html/body/form/input[1]
Why does ends-with() fail in Selenium?
Because browser XPath implementations support XPath 1.0, which does not provide the XPath 2.0 ends-with() function.
For CSS, use $=:
input[id$='-name']
For XPath, use the substring() workaround shown earlier.
What are XPath axes?
XPath axes provide ways to navigate from one element to related elements in the DOM.
Common axes include:
parentchildancestordescendantfollowing-siblingpreceding-siblingfollowingprecedingself
They are especially useful when the target element does not have a reliable unique attribute.
How do you locate dynamic elements?
Start by identifying what remains stable.
You can:
- Match a stable portion of an attribute.
- Use
contains()orstarts-with()in XPath. - Use CSS attribute selectors.
- Find a stable nearby element and navigate with XPath axes.
- Use explicit waits when the element is loaded dynamically.
CSS or XPath — which should I use?
CSS is concise and works particularly well for attributes, classes and DOM structure.
XPath becomes more useful when you need:
- Text matching
- Parent navigation
- Sibling navigation
- Ancestor/descendant relationships
- More complex DOM relationships
The best choice depends on the structure of the application and the stability of the locator.