Selenium Locators Cheat Sheet
Every Selenium locator you need on one page. All XPath and CSS examples were tested against the small sample page below, so you can see exactly what each one matches. For explanations and 150+ practice exercises, see Selenium Locators & XPath.
Sample HTML
<div id="main">
<form id="login-form">
<label>Email</label>
<input type="text" id="user_4821" name="email" class="form-control input-lg">
<label>Password</label>
<input type="password" id="pass_name" name="password" class="form-control">
<button type="submit" class="btn btn-primary"> Sign in </button>
</form>
<table id="users">
<tr><td>Asha</td><td>QA</td><td><button>Edit</button></td></tr>
<tr><td>John</td><td>DEV</td><td><button>Edit</button></td></tr>
</table>
<ul><li>One</li><li>Two</li><li>Three</li></ul>
<div id="msg_17" class="alert error">Invalid</div>
</div>
The 8 Locator Strategies
| Strategy | Syntax | Best used when |
|---|---|---|
| id | By.id("user_4821") |
The ID is unique and stable — the first choice |
| name | By.name("email") |
Form fields with a unique name |
| className | By.className("btn-primary") |
One class identifies the element — single class only |
| tagName | By.tagName("a") |
Collecting all elements of a type (links, rows) |
| linkText | By.linkText("Sign In") |
Links with exact visible text |
| partialLinkText | By.partialLinkText("Sign") |
Links with partial or changing text |
| cssSelector | By.cssSelector("#login-form input[name='email']") |
Fast, concise, most attribute-based cases |
| xpath | By.xpath("//label[text()='Email']/following-sibling::input[1]") |
Text matching and moving up or sideways in the DOM |
Priority: stable test attribute (data-testid) or id → name → CSS → XPath. XPath isn't slower in any way that matters in modern browsers — choose it when you need text or DOM relationships.
className trap: By.className("form-control input-lg") throws InvalidSelectorException (compound class names aren't allowed). Use CSS: By.cssSelector(".form-control.input-lg").
XPath Cheat Sheet
| Pattern | Example | Matches in the sample |
|---|---|---|
| Attribute | //input[@name='email'] |
Email input |
| Multiple attributes | //input[@type='text' and @name='email'] |
Email input |
| Exact text | //td[text()='John'] |
The "John" cell |
| Text with spaces ⭐ | //button[normalize-space()='Sign in'] |
Sign-in button (text()='Sign in' matches nothing — the text has surrounding spaces) |
| contains() | //div[contains(@class,'error')] |
Error message |
| starts-with() | //input[starts-with(@id,'user_')] |
Email input |
| Scoped to a container | //form[@id='login-form']//input[@type='text'] |
Email input |
| Nth match overall | (//table//tr)[2]/td[1] |
"John" |
| Last / position | //ul/li[last()] · //ul/li[position()<3] |
"Three" · "One", "Two" |
| Any element | //*[@id='main'] |
The main div |
Selenium supports XPath 1.0 only: ends-with(), matches() and lower-case() don't work. Use contains(), or CSS $= for "ends with". Avoid absolute XPath (/html/body/…) — see Absolute vs Relative XPath.
XPath Axes ⭐
| Axis | Finds | Family analogy |
|---|---|---|
parent:: |
Direct parent | Your parent |
child:: |
Direct children | Your children |
ancestor:: |
Parent, grandparent and above | Parents and grandparents |
descendant:: |
Children and below | Children and grandchildren |
following-sibling:: |
Later siblings | Younger siblings |
preceding-sibling:: |
Earlier siblings | Older siblings |
following:: / preceding:: |
Anything after / before in the page | Everyone after / before you |
self:: |
The node itself | You |
//td[text()='John']/following-sibling::td[1] → "DEV" (the next cell in John's row)
//td[text()='John']/ancestor::tr//button[text()='Edit'] → John's Edit button
//label[text()='Email']/following-sibling::input[1] → the input after the Email label
//input[@id='pass_name']/parent::form → the login form
A common mistake: //td[text()='John']/ancestor::tr/following-sibling::td matches nothing — the siblings of a tr are other rows, not cells. Go up to the row, then back down (//) to the cell you need.
CSS Selector Cheat Sheet
| Pattern | Example | Matches in the sample |
|---|---|---|
| ID | #user_4821 |
Email input |
| Class / several classes | .btn-primary · .form-control.input-lg |
Sign-in button · email input |
| Tag + class | input.form-control |
Both inputs |
| Attribute | input[name='email'] |
Email input |
| Several attributes | input[type='text'][name='email'] |
Email input |
| Contains / starts / ends with | div[class*='error'] · input[id^='user_'] · input[id$='_name'] |
Error div · email input · password input |
| Direct child / any descendant | #login-form > input · #login-form input |
Both inputs |
| Position | li:nth-child(2) · li:last-child |
"Two" · "Three" |
| Negation | input:not([type='password']) |
Email input |
CSS limits: it can't match by visible text and can't move up to a parent — use XPath for those. (:has() exists in modern browsers but isn't reliable across all drivers.)
Relative Locators (Selenium 4)
import static org.openqa.selenium.support.locators.RelativeLocator.with;
WebElement password = driver.findElement(with(By.tagName("input")).below(By.name("email")));
WebElement label = driver.findElement(with(By.tagName("label")).toLeftOf(By.id("user_4821")));
// Also: above(), toRightOf(), near()
Useful when an element has nothing unique but sits next to something that does. They depend on screen layout, so they can change with window size.
Dynamic Elements ⭐
//input[starts-with(@id,'user_')] ← stable prefix of a generated ID
//div[contains(@id,'msg_')] ← stable part anywhere
//label[text()='Email']/following-sibling::input[1] ← anchored to stable text
//form[@id='login-form']//input[@type='text'] ← scoped to a stable container
[data-testid='login-email'] ← best of all: a test attribute
Ask developers for data-testid attributes — they're the most reliable fix for dynamic IDs. Scenario answers: Selenium Scenario Questions.
Shadow DOM
SearchContext shadow = driver.findElement(By.cssSelector("app-login")).getShadowRoot();
shadow.findElement(By.cssSelector("input[name='email']")).sendKeys("asha@example.com");
Inside a shadow root, use CSS selectors — XPath isn't supported there.
findElement() vs findElements()
findElement() |
findElements() |
|
|---|---|---|
| Returns | One WebElement (the first match) |
List<WebElement> |
| No match | Throws NoSuchElementException |
Returns an empty list |
| Use for | Interacting with one element | Counting, looping, checking absence |
int totalLinks = driver.findElements(By.tagName("a")).size();
boolean errorShown = !driver.findElements(By.cssSelector(".error")).isEmpty(); // absence check without an exception
Test Your Locator Before Using It
- Chrome DevTools → Elements → Ctrl+F (Cmd+F on Mac) accepts XPath and CSS and shows the match count — aim for exactly "1 of 1".
- Console:
$x("//button[normalize-space()='Sign in']")for XPath,$$("div[class*='error']")for CSS. - Or use the Selector Playground, and see how a found element is used step by step in the Selenium WebDriver Visualizer.
From Real Projects
Across Apkope, Canolog and Testsigma I've done both manual testing and Selenium automation in Java with TestNG, tracked execution in TestRail and defects in Jira, and worked in Agile teams. The topics on this page connect directly to that day-to-day work. Try id and name first, then CSS, and XPath when nothing simpler works.
📚 Official documentation: Selenium docs: Locator strategies · Selenium documentation
FAQs
Which Selenium locator is best?
A stable, unique id or data-testid. After that, name, then CSS, then XPath for text or relationship-based locating.
CSS selector or XPath?
CSS is shorter for attribute-based locators; XPath can match visible text and move up or sideways in the DOM. Use whichever makes the locator simplest and most stable.
Why does By.className fail with two classes?
It accepts a single class name only. For several classes use CSS: .class1.class2.
Why doesn't //button[text()='Sign in'] match?
The text includes spaces or line breaks. Use normalize-space()='Sign in' or contains(text(),'Sign in').
How do you handle dynamic IDs?
Match the stable part with starts-with()/contains() (or CSS ^=, *=), anchor to stable text or a container, or use a test attribute.
Can XPath be used inside a shadow DOM?
No — use getShadowRoot() and CSS selectors.