Introduction to Locators
Locators tell your automation tool which element to interact with — click a button, type in an input, assert text, and more.
Why locators matter
Every Selenium, Playwright, Cypress, or Appium (web) script finds elements before it can act on them. A wrong or brittle locator makes tests fail even when the application works correctly.
What you will learn here
- All major locator strategies used in test automation
- How to build CSS selectors and XPath expressions
- How to locate inputs, buttons, dropdowns, tables, links, and more
- How to handle iframes, Shadow DOM, and dynamic elements
- Syntax differences across Selenium, Playwright, and Cypress
- Best practices so locators stay stable
Locator strategies overview
| Strategy |
Example |
Best for |
| ID |
#login-btn |
Unique, stable elements |
| Name |
[name="email"] |
Form fields |
| Class Name |
.btn-primary |
Styled groups (use carefully) |
| Tag Name |
button |
Broad matches, rarely alone |
| Link Text |
Forgot Password? |
Exact link text |
| Partial Link Text |
Forgot |
Partial link text |
| CSS Selector |
button.btn-primary |
Fast, readable, most cases |
| XPath |
//button[text()='Submit'] |
Text, hierarchy, complex logic |
| data-testid |
[data-testid="submit"] |
Test-friendly (recommended) |
Use the left menu to study each topic. Hands-on UI practice is on the main Practice Hub page.
HTML & DOM Basics for Testers
You must understand HTML before writing good locators. Automation tools search the DOM (Document Object Model).
What is the DOM?
The DOM is a tree of HTML elements. Locators navigate this tree to find nodes (elements).
<form id="login-form">
<label for="email">Email</label>
<input type="email" id="email" name="email" class="form-control" data-testid="email-input">
<button type="submit" class="btn btn-primary">Login</button>
</form>
Key terms
| Term |
Meaning |
Example |
| Tag |
Element type |
input, button, div |
| Attribute |
Extra info on a tag |
id, name, class, type |
| Value |
What the attribute holds |
id="email" |
| Text |
Visible text inside a tag |
Login inside <button> |
| Parent / Child |
Nesting relationship |
form is parent of input |
Attributes automation uses most
id — unique identifier
name — form field name
class — CSS class (may be shared)
type — e.g. text, email, password, submit
href — link URL
placeholder — hint text in inputs
value — current value of input/button
disabled / readonly — element state
data-testid — attribute added for testing
aria-label — accessibility label (also useful for locators)
In Chrome: right-click element → Inspect. Hover attributes to understand structure before writing locators.
ID Locator
ID is the most preferred built-in locator when the value is unique and stable.
What is an ID?
The id attribute identifies one element on the page. In HTML, each id should be unique.
<input type="email" id="email-input" placeholder="you@example.com">
<button id="btn-submit">Submit</button>
How to write it
| Style |
Syntax |
| CSS |
#email-input |
| XPath |
//*[@id="email-input"] |
| Selenium |
By.id("email-input") |
| Playwright |
page.locator("#email-input") |
| Cypress |
cy.get("#email-input") |
When to use ID
- ID exists and does not change between builds
- ID is unique on the page
- You want the fastest, simplest locator
When to avoid ID
- Auto-generated IDs:
id="react-aria-12", id="ember45"
- IDs that include timestamps or random numbers
- Duplicate IDs (invalid HTML — locators become unreliable)
If ID looks random or changes after refresh, do not use it. Prefer data-testid, name, or a stable CSS/XPath.
Name Locator
The name attribute is common on form fields. Useful for login, register, and search forms.
What is Name?
HTML forms use name so data can be submitted. Multiple elements can share the same name (e.g. radio buttons).
<input type="text" name="username" id="login-username">
<input type="password" name="password" id="login-password">
<input type="radio" name="gender" value="male">
<input type="radio" name="gender" value="female">
How to write it
| Style |
Syntax |
| CSS |
[name="username"] or input[name="username"] |
| XPath |
//input[@name="username"] |
| Selenium |
By.name("username") |
| Playwright |
page.locator('[name="username"]') |
| Cypress |
cy.get('[name="username"]') |
Important notes
- If several elements share a name,
By.name returns the first match in Selenium unless you use findElements.
- For radio groups, combine name with value:
input[name="gender"][value="male"]
- Name is often more stable than class, less unique than id.
Class Name & Tag Name
Useful helpers, but rarely the best primary locator alone.
Class Name
The class attribute can hold one or many CSS classes. Many elements often share the same class.
<button class="btn btn-primary">Save</button>
<button class="btn btn-secondary">Cancel</button>
| Style |
Syntax |
| CSS |
.btn-primary |
| XPath |
//button[contains(@class,"btn-primary")] |
| Selenium |
By.className("btn-primary") |
Class Name pitfalls
- Selenium
By.className accepts only one class — not "btn btn-primary"
- Classes change when designers update UI
- Many matches → flaky tests
Tag Name
Finds elements by HTML tag: input, button, a, table, etc.
| Style |
Syntax |
| CSS |
button, input |
| XPath |
//button, //input |
| Selenium |
By.tagName("button") |
Tag name alone is too broad. Combine with attributes: button.btn-primary or input[type="email"].
Link Text & Partial Link Text
Used for <a> (anchor) tags when the visible text is unique and stable.
Example HTML
<a href="/forgot">Forgot Password?</a>
<a href="/signup">Create new account</a>
<a href="next-page.html">Next Page</a>
Link Text (exact match)
| Tool |
Syntax |
| Selenium |
By.linkText("Forgot Password?") |
| XPath |
//a[text()="Forgot Password?"] |
| Playwright |
page.getByRole("link", { name: "Forgot Password?" }) |
Partial Link Text
| Tool |
Syntax |
| Selenium |
By.partialLinkText("Forgot") |
| XPath |
//a[contains(text(),"Forgot")] |
| CSS |
CSS cannot match by text (use XPath or role/text APIs) |
When to avoid
- Link text changes with language (i18n)
- Text is not unique
- Extra spaces or nested tags break exact text match
CSS Selectors
CSS selectors are fast, widely supported, and preferred for most automation cases (when not locating by text).
Basic selectors
| Selector |
Meaning |
Example |
#id |
By id |
#submit-btn |
.class |
By class |
.btn-primary |
tag |
By tag |
button |
* |
Any element |
form * |
[attr] |
Has attribute |
[disabled] |
[attr="val"] |
Attribute equals |
[type="email"] |
[attr*="val"] |
Attribute contains |
[class*="primary"] |
[attr^="val"] |
Attribute starts with |
[id^="otp-"] |
[attr$="val"] |
Attribute ends with |
[href$=".pdf"] |
Combinators
div button — descendant (any depth)
div > button — direct child only
label + input — adjacent sibling (next)
label ~ input — general siblings after
Pseudo-classes useful in testing
:nth-child(2) — second child
:nth-of-type(3) — third of that tag type
:first-child / :last-child
:disabled / :enabled / :checked
:not(.hidden) — exclude a class
How to build a CSS selector (step by step)
- Inspect the element in DevTools.
- Prefer unique attributes:
id, data-testid, name.
- If not unique, add tag + attribute:
input[name="email"].
- If still not unique, add parent context:
#login-form input[name="email"].
- Avoid long chains of classes and
nth-child unless necessary.
Practical examples
/* Email field */
#email-input
input[type="email"]
[data-testid="email-input"]
/* Primary button inside a form */
#login-form button.btn-primary
/* First OTP digit */
.otp-inputs input:nth-child(1)
[data-testid="otp-digit-1"]
/* Disabled button */
button:disabled
#btn-disabled:disabled
/* Table cell in row 2, column 3 */
table tbody tr:nth-child(2) td:nth-child(3)
CSS limitation
CSS cannot select by visible text (e.g. button that says "Submit"). Use XPath, Playwright getByText / getByRole, or Cypress contains().
XPath Basics
XPath navigates the HTML tree. Use it for text matching, complex conditions, and moving between parent/child/sibling nodes.
What is XPath?
XPath (XML Path Language) is a query language for selecting nodes. In automation it works on HTML too.
Two types of path
| Type |
Example |
Notes |
| Absolute |
/html/body/div[1]/form/input[2] |
Starts from root. Brittle — avoid. |
| Relative |
//input[@id="email"] |
Starts anywhere with //. Preferred. |
Core syntax
//tag — any matching tag anywhere
//tag[@attr="value"] — attribute equals
//*[@id="email"] — any tag with that id
//button[text()="Submit"] — exact text
//button[contains(text(),"Submit")] — partial text
//input[@type="text" and @name="q"] — multiple conditions
//div[@class="card"]//button — descendant button
How to make an XPath (step by step)
- Find a unique attribute: id, name, data-testid, type, placeholder.
- Write:
//tag[@attribute="value"]
- If locating by text:
//tag[text()="exact"] or contains(text(),"part")
- If not unique, add a stable parent:
//form[@id="login"]//input[@name="email"]
- Test in Chrome Console:
$x('//your/xpath')
Common patterns
| Goal |
XPath |
| By id |
//*[@id="email-input"] |
| By name |
//input[@name="password"] |
| By class (contains) |
//button[contains(@class,"btn-primary")] |
| By exact text |
//button[text()="Primary Button"] |
| By partial text |
//a[contains(text(),"Next")] |
| By placeholder |
//input[@placeholder="Search..."] |
| Index (1-based) |
(//button[@class="btn"])[2] |
Prefer contains(@class,"...") over @class="exact full string" because class lists often have multiple values.
XPath Advanced
Axes, functions, and patterns for complex pages.
XPath axes
| Axis |
Meaning |
Example |
parent |
Parent node |
//input[@id="email"]/parent::div |
child |
Direct children |
//ul[@id="menu"]/child::li |
ancestor |
All parents up |
//input[@id="email"]/ancestor::form |
descendant |
All nested nodes |
//form/descendant::input |
following-sibling |
Next siblings |
//label[@for="email"]/following-sibling::input |
preceding-sibling |
Previous siblings |
//input[@id="email"]/preceding-sibling::label |
following |
Everything after in document |
//h2/following::button[1] |
preceding |
Everything before |
//button[@id="submit"]/preceding::input[1] |
Useful XPath functions
contains(string, substring) — partial match
starts-with(string, prefix) — starts with
text() — element text node
normalize-space() — trim extra spaces in text
position() — position among siblings
last() — last matching node
not() — negation
Advanced examples
// Label → related input
//label[text()="Email"]/following-sibling::input
//label[@for="email"]/../input
// Text with extra spaces
//button[normalize-space()="Submit"]
// Dynamic id prefix
//*[starts-with(@id,"otp-digit-")]
// Not disabled
//button[not(@disabled)]
// Last row in table
//table[@id="users"]//tr[last()]
// Cell by header context (simple pattern)
//td[text()="John"]/following-sibling::td[1]
// AND / OR
//input[@type="text" or @type="email"]
//button[@type="submit" and contains(@class,"primary")]
Chained / relative XPath from a found element
In Selenium you can find a parent, then search inside it:
WebElement form = driver.findElement(By.id("login-form"));
WebElement email = form.findElement(By.xpath(".//input[@name='email']"));
# Note the leading "." — search relative to form, not whole page
data-testid & Custom Attributes
The most automation-friendly approach when developers can add test attributes.
Why data-testid?
- Created for testing — not for styling or layout
- Usually stable across redesigns
- Clear intent: this attribute is for automation
<input data-testid="email-input" type="email">
<button data-testid="btn-submit">Submit</button>
<div data-testid="toast-success">Saved!</div>
How to locate
| Tool |
Syntax |
| CSS |
[data-testid="email-input"] |
| XPath |
//*[@data-testid="email-input"] |
| Playwright |
page.getByTestId("email-input") |
| Cypress |
cy.get('[data-testid="email-input"]') |
| Selenium |
By.cssSelector("[data-testid='email-input']") |
Other useful custom attributes
data-qa, data-cy, data-test — same idea, different naming
aria-label — good for accessibility and role-based locators
role — used by Playwright getByRole
On QA Practice Hub, almost every interactive element has a data-testid. Prefer it when writing scripts against this site.
Absolute vs Relative Locators
Understand the difference — this is a common interview and real-project topic.
Absolute (full path)
/html/body/div[2]/div[1]/form/div[3]/input
- Starts from
/html
- Breaks if any parent structure changes
- Hard to read and maintain
- Avoid in real projects
Relative (recommended)
//input[@id="email-input"]
#login-form [name="email"]
[data-testid="email-input"]
- Starts from a unique attribute or nearby context
- Survives layout changes better
- Easier to review in code reviews
Relative from a parent (scoping)
Find a container first, then search inside it. This avoids matching wrong elements elsewhere on the page.
CSS: #otp-login-card input[name="username"]
XPath: //div[@id="otp-login-card"]//input[@name="username"]
UI Elements Locator Guide
How to locate the UI types automation testers work with every day.
Text / Email / Password inputs
CSS: input[type="text"], #text-input, [name="email"]
XPath: //input[@type="password"]
Tip: Prefer id / name / data-testid over placeholder alone
Buttons
CSS: button.btn-primary, #btn-submit, button[type="submit"]
XPath: //button[text()="Submit"]
//button[normalize-space()="Primary Button"]
Note: Disabled → button:disabled or //button[@disabled]
Checkboxes & Radio buttons
CSS: input[type="checkbox"][name="terms"]
input[type="radio"][value="male"]
XPath: //input[@type="radio" and @value="female"]
Assert checked: CSS input:checked
Dropdowns (select)
CSS: select#country, select[name="country"]
XPath: //select[@id="country"]/option[@value="IN"]
Select by visible text in Selenium:
new Select(element).selectByVisibleText("India");
Links
CSS: a[href="next-page.html"]
XPath: //a[text()="Next Page"]
//a[contains(@href,"next-page")]
Tables
/* Header cell */
table thead th:nth-child(2)
/* Body row 3, column 1 */
table tbody tr:nth-child(3) td:nth-child(1)
XPath: //table[@id="users"]//tr[td[text()="Alice"]]/td[2]
Modals / dialogs
CSS: .modal, [role="dialog"], #forgot-password-modal
Tip: Scope locators inside the modal so you don't hit hidden duplicates
Alerts (browser native)
Native alert / confirm / prompt are not DOM elements. Switch to alert API — no CSS/XPath.
Selenium: driver.switchTo().alert()
Playwright: page.on("dialog", ...)
Cypress: cy.on("window:alert", ...)
File upload
CSS: input[type="file"]
Send path via sendKeys / setInputFiles — do not click OS file dialog
OTP / multi-input fields
CSS: #otp-digit-1, .otp-digit:nth-child(1)
XPath: //div[@data-testid="otp-inputs"]/input[1]
Tip: Prefer individual data-testid per digit
iFrames
Elements inside an iframe are in a different document. You must switch context before locating them.
What is an iframe?
<iframe id="demo-frame" src="iframe-content.html"></iframe>
How to work with iframes
- Locate the iframe element (by id, name, CSS, or index).
- Switch into the iframe.
- Locate and interact with elements inside.
- Switch back to the main page when done.
Examples
Selenium:
driver.switchTo().frame("demo-frame");
driver.findElement(By.id("inside-btn")).click();
driver.switchTo().defaultContent();
Playwright:
const frame = page.frameLocator("#demo-frame");
await frame.locator("#inside-btn").click();
Cypress:
cy.get("#demo-frame").its("0.contentDocument.body").find("#inside-btn").click();
If locators work in DevTools on the iframe document but fail in your script, you forgot to switch into the iframe.
Shadow DOM
Shadow DOM encapsulates components. Normal CSS/XPath often cannot pierce into closed/open shadow trees the same way.
Concept
A host element owns a shadow root. Elements inside the shadow root are hidden from normal document queries unless you pierce the shadow.
How tools handle it
| Tool |
Approach |
| Selenium 4+ |
By.shadowDomCss(...) / piercing selectors (version-dependent) |
| Playwright |
CSS piercing works: page.locator("host-el >> inner-el") or locator("host").locator("inner") |
| Cypress |
Plugins or .shadow() command |
Playwright example:
await page.locator("#shadow-host").locator(".inner-button").click();
JS evaluate (concept):
document.querySelector("#shadow-host").shadowRoot.querySelector(".inner-button")
Practice Shadow DOM on the main hub under the Advanced section.
Dynamic Elements & Waits
Elements that load late, change IDs, or appear after actions need careful locators and waits.
Common dynamic patterns
- IDs with random suffixes:
user_ab12cd
- Elements created after API response
- Lists that grow/shrink (add/remove items)
- Stale element references after re-render
Locator strategies for dynamic IDs
CSS: [id^="user_"] /* starts with */
[id*="user"] /* contains */
XPath: //*[starts-with(@id,"user_")]
//*[contains(@id,"user")]
Waits (do not use Thread.sleep blindly)
| Type |
Meaning |
| Implicit wait |
Global timeout for findElement (use carefully) |
| Explicit wait |
Wait for a condition on a specific element |
| Fluent wait |
Explicit wait with polling + ignored exceptions |
Selenium:
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.visibilityOfElementLocated(By.id("toast")));
Playwright:
await expect(page.getByTestId("toast")).toBeVisible();
Cypress:
cy.get("[data-testid=toast]", { timeout: 10000 }).should("be.visible");
Stale Element Reference
Happens when the DOM was refreshed and your stored WebElement is outdated. Re-find the element instead of reusing the old reference.
For attributes that change on a timer while the element stays in the DOM, practice on the hub: Changing Attributes.
Locator Best Practices
Rules that keep automation suites stable.
Priority order (recommended)
data-testid / test attributes
- ID (if stable and unique)
- Name (forms)
- Role + accessible name (Playwright)
- CSS selector (short and specific)
- XPath (when text/hierarchy is required)
- Link text / index / absolute path (last resorts)
Do
- Keep locators short and readable
- Scope to a parent container when needed
- Store locators in one place (Page Object Model)
- Wait for visibility/clickability before interacting
- Ask developers for
data-testid on critical elements
Don't
- Don't copy absolute XPath from DevTools blindly
- Don't depend on long class chains used for styling
- Don't use index (
[3]) unless the order is guaranteed
- Don't hardcode sleeps instead of proper waits
- Don't use text that changes with language unless intentional
Page Object Model (POM) tip
// Keep locators in page classes
class LoginPage {
email = "#email-input";
password = "[name='password']";
submit = "[data-testid='btn-submit']";
}
Chrome DevTools Tips for Locators
Practice and validate locators before putting them in scripts.
Inspect an element
- Right-click the element → Inspect
- Look at tag, id, name, class, data-testid
- Right-click the node in Elements → Copy → Copy selector / Copy XPath (then improve it manually)
Test CSS in Console
document.querySelector("#email-input")
document.querySelectorAll(".otp-digit")
$("button.btn-primary") // Chrome shortcut for querySelector
$$(".btn") // Chrome shortcut for querySelectorAll
Test XPath in Console
$x("//button[text()='Submit']")
$x("//input[@name='password']")
$x("//div[@id='otp-login']//input").length
Highlight matches while designing
In Elements panel, press Ctrl+F / Cmd+F and paste a CSS or XPath. Chrome highlights matches and shows the count.
Checklist before finalizing a locator
- Matches exactly 1 element (unless you need a list)
- Still works after page refresh
- Does not depend on random IDs
- Readable for the next engineer
After studying here, open the Practice Hub and write real scripts against Inputs, Buttons, OTP Login, Tables, and more.