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.

Advertisement

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.

Go Deeper