On this page8 sections
ReferenceBeginner5-7 min reference

XPath & CSS Selectors

Most modern frameworks (Cypress, Playwright, WebDriverIO) prefer CSS selectors. XPath is still essential for Selenium and for cases where text content or DOM traversal is the only reliable hook.

CSS Basics

PatternMatches
buttonAll <button> elements
#submitElement with id="submit"
.primaryElements with class primary
[data-testid="login"]Element with that test id
[type="email"]Inputs of type email
a[href^="/docs"]Links whose href starts with /docs
a[href$=".pdf"]Links to PDF files
img[alt*="logo"]Images with "logo" anywhere in alt
*Universal selector — every element

CSS Combinators

/* Descendant — any depth */
form input
 
/* Direct child */
ul > li
 
/* Adjacent sibling */
h2 + p
 
/* General sibling */
h2 ~ p

CSS Pseudo-classes

li:first-child
li:last-child
li:nth-child(3)
li:nth-child(odd)
li:nth-child(2n + 1)
li:nth-of-type(2)
 
input:checked
input:disabled
input:focus
input:placeholder-shown
input:invalid
 
a:hover
a:not([href])
a:not(.external)
 
/* :has() — modern, parent-of selector */
form:has(input:invalid)
li:has(> a.active)

XPath Basics

//button                        ← any <button> in the document
/html/body/div                  ← absolute path (avoid)
//div[@id='main']               ← element with attribute
//input[@type='email']          ← attribute equality
//a[@href='/login']
//*[@data-testid='submit']      ← any tag with attribute

XPath Functions

//button[text()='Submit']            ← exact text match
//button[contains(text(), 'Sub')]    ← substring match
//a[starts-with(@href, '/docs')]     ← prefix match
//div[contains(@class, 'card')]      ← class contains (XPath has no class shortcut)
//input[normalize-space(text())='OK'] ← collapses whitespace
//li[position()=1]                   ← first
//li[last()]                         ← last
//li[position() < 4]                 ← first 3

XPath Axes

XPath's superpower — traversing the tree from an anchor element.

//label[text()='Email']/following-sibling::input    ← input next to a label
//input[@id='email']/preceding-sibling::label       ← label before an input
//td[text()='42']/parent::tr                        ← parent row of a cell
//a[text()='Edit']/ancestor::tr                     ← row containing a link
//ul[@class='nav']/descendant::a                    ← all links inside nav

Side-by-side

GoalCSSXPath
By id#login//*[@id='login']
By class.btn-primary//*[contains(@class,'btn-primary')]
By attribute[data-testid="x"]//*[@data-testid='x']
By textn/a — use library helper//*[text()='Submit']
Contains textn/a//*[contains(text(),'Sub')]
Direct childul > li//ul/li
Descendantul li//ul//li
Nth-childli:nth-child(2)//li[2]
Has childform:has(input.error)//form[.//input[contains(@class,'error')]]
Siblinglabel + input//label/following-sibling::input[1]
Parentn/a//input/parent::label

Quick guidance

  • Prefer data-testid (or any test-only attribute) over class or text — it survives refactors.
  • Avoid absolute XPath (/html/body/div[3]/...) — it breaks on any DOM change.
  • Use text-based selectors only for visible UI labels you control.
  • In Playwright, prefer built-in locators (getByRole, getByLabel, getByTestId) over raw selectors.