XPath vs CSS Selectors
The quick reference every automation engineer keeps open — every common locator in CSS and XPath, side by side. Works for Selenium and Playwright.
🧠Rule of thumb: CSS for speed & readability · XPath when you need text or to walk up the DOM.
Basics & attributes
| Goal | CSS | XPath |
|---|---|---|
| Any element | * | //* |
| By tag | input | //input |
| By id | #username | //*[@id='username'] |
| By class (in list) | .btn | //*[contains(@class,'btn')] |
| Multiple classes | .btn.primary | //*[contains(@class,'btn') and contains(@class,'primary')] |
| By attribute | [type='text'] | //*[@type='text'] |
| Tag + attribute | input[name='email'] | //input[@name='email'] |
| data-testid (preferred hook) | [data-testid='submit'] | //*[@data-testid='submit'] |
| Attribute starts-with | [id^='user'] | //*[starts-with(@id,'user')] |
| Attribute contains | [class*='card'] | //*[contains(@class,'card')] |
| Attribute ends-with | [id$='_btn'] | no ends-with in XPath 1.0 |
Hierarchy & relationships
| Goal | CSS | XPath |
|---|---|---|
| Descendant (any depth) | div input | //div//input |
| Direct child | ul > li | //ul/li |
| Next sibling (adjacent) | label + input | //label/following-sibling::input[1] |
| Any following sibling | label ~ input | //label/following-sibling::input |
| Parent | not supported | //span/parent::div · //span/.. |
| Ancestor | not supported | //td/ancestor::table |
| Group / OR | h1, .title | //h1 | //*[contains(@class,'title')] |
Text & position
| Goal | CSS | XPath |
|---|---|---|
| Exact text | not supported | //button[text()='Submit'] |
| Contains text | not supported | //a[contains(text(),'Login')] |
| Normalized text (trims spaces) | not supported | //h1[normalize-space()='Title'] |
| First match | li:first-child | (//input)[1] · //ul/li[1] |
| Nth element | li:nth-child(2) | //li[2] |
| Last element | li:last-child | //li[last()] |
| Negation | input:not([disabled]) | //input[not(@disabled)] |
Pro tips
- Prefer CSS for speed and readability. Reach for XPath only when you need text() matching or to traverse upward (parent / ancestor) — CSS can't do either.
- Lock onto stable hooks: id, name, and especially data-testid. Avoid absolute paths like /html/body/div[2]/… — they shatter on any layout change.
- CSS .btn matches an element whose class list contains 'btn'; XPath [@class='btn'] is an exact match. Use contains(@class,'btn') in XPath to mimic CSS.
- Selenium uses XPath 1.0 — there's no ends-with(). Use starts-with() or contains() instead.
- On modern Playwright / Testing-Library, prefer getByRole / getByText / getByTestId over raw selectors — more resilient and accessible by default.
Going for an automation interview?
Selectors are just the start. Get the full Selenium, Playwright & Python kits — Q&A, coding, and résumé templates.
🔥 See the SDET Kits