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
| Goal | XPath | CSS |
|---|---|---|
| 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
| Goal | XPath | CSS |
|---|---|---|
| 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)
| Goal | XPath | CSS |
|---|---|---|
| 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
| Goal | XPath | CSS |
|---|---|---|
| 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
| Goal | XPath | CSS |
|---|---|---|
| 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)
| Goal | XPath | CSS |
|---|---|---|
| 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
| Task | Code |
|---|---|
| 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 |