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
| Pattern | Matches |
|---|---|
button | All <button> elements |
#submit | Element with id="submit" |
.primary | Elements 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 ~ pCSS 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 attributeXPath 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 3XPath 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 navSide-by-side
| Goal | CSS | XPath |
|---|---|---|
| By id | #login | //*[@id='login'] |
| By class | .btn-primary | //*[contains(@class,'btn-primary')] |
| By attribute | [data-testid="x"] | //*[@data-testid='x'] |
| By text | n/a — use library helper | //*[text()='Submit'] |
| Contains text | n/a | //*[contains(text(),'Sub')] |
| Direct child | ul > li | //ul/li |
| Descendant | ul li | //ul//li |
| Nth-child | li:nth-child(2) | //li[2] |
| Has child | form:has(input.error) | //form[.//input[contains(@class,'error')]] |
| Sibling | label + input | //label/following-sibling::input[1] |
| Parent | n/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.