Skip to content
SDETRole
SDETRole

Learn / Cheat sheet

XPath and CSS Selectors Cheat Sheet

XPath and CSS side by side, for Selenium, Playwright and browser DevTools. Use CSS when it can express what you need, since it is shorter and the syntax developers know. Reach for XPath when you need to match visible text or walk up the tree to a parent or ancestor.

Checked against XPath 1.0 (what browsers implement), CSS Selectors Level 4 as supported by current Chrome, Firefox and Safari

Basics

GoalXPathCSS
Element by tag (anywhere) //button button
By id //*[@id='email'] #email
By class (one of several) //*[contains(concat(' ', normalize-space(@class), ' '), ' active ')] .active
Direct child //ul/li ul > li
Any descendant //form//input form input
Several selectors (OR) //h1 | //h2 h1, h2
Any element //* *

Attributes

GoalXPathCSS
Has attribute //button[@disabled] button[disabled]
Equals //input[@name='q'] input[name='q']
Contains //a[contains(@href, 'orders')] a[href*='orders']
Starts with //a[starts-with(@href, 'https')] a[href^='https']
Ends with //a[substring(@href, string-length(@href) - 3) = '.pdf'] a[href$='.pdf']
Two conditions (AND) //input[@type='text' and @required] input[type='text'][required]
Either condition (OR) //input[@type='email' or @type='tel'] input[type='email'], input[type='tel']
Not //input[not(@disabled)] input:not([disabled])
Test id //*[@data-testid='submit'] [data-testid='submit']

XPath 1.0 has no ends-with(): the substring() form is the portable workaround.

Text (XPath only)

GoalXPathCSS
Exact text, whitespace normalised //button[normalize-space()='Sign in'] not possible
Text contains //p[contains(., 'expired')] not possible
Case-insensitive contains //p[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'expired')] not possible
Own text node only //label[text()='Email'] not possible

. (the string value) includes text in child elements; text() only matches the element’s own text nodes. Prefer normalize-space() or .

Relationships and axes

GoalXPathCSS
Parent //input[@id='email']/.. :has(> #email)
Ancestor row of a cell //td[normalize-space()='#1043']/ancestor::tr tr:has(td[data-id='1043'])
Next sibling //label[.='Password']/following-sibling::input[1] label + input
All following siblings //h2/following-sibling::p h2 ~ p
Previous sibling //button/preceding-sibling::input[1] not possible
Cell next to a label cell //td[.='Diego Ramos']/following-sibling::td[1] not possible (starts from text)
Element containing a child //li[.//button[@aria-label='Remove']] li:has(button[aria-label='Remove'])

:has() works in current Chrome, Edge, Safari and Firefox (121+). Older browsers and some tools reject it.

Position

GoalXPathCSS
First child row (1-based) //tbody/tr[1] tbody tr:first-child
Nth row //tbody/tr[3] tbody tr:nth-child(3)
Last row //tbody/tr[last()] tbody tr:last-child
Second-to-last //tbody/tr[last()-1] tbody tr:nth-last-child(2)
Every even row //tbody/tr[position() mod 2 = 0] tbody tr:nth-child(even)
First match in the whole page (//button)[1] not possible: use your tool’s first()

//tr[1] means "every tr that is the first tr in its parent", not "the first tr on the page". Use (//tr)[1] for that.

State pseudo-classes (CSS)

GoalXPathCSS
Checked box or radio //input[@checked] (initial state only) input:checked
Disabled //button[@disabled] button:disabled
Required field //input[@required] input:required
Empty element //td[not(node())] td:empty

XPath reads attributes as written in the HTML, so @checked does not change when a user ticks the box. CSS :checked follows the live state.

Try them in DevTools

TaskCode
CSS, all matches (Console) $$('tbody tr')
XPath, all matches (Console) $x("//td[.='Mei Lin']")
Search the Elements panel Ctrl+F / Cmd+F, then type a selector or XPath
Count matches $$('tbody tr').length

Practise and go deeper

More cheat sheets

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →