Cheatsheet Β· Updated 2026-10-23
XPath and CSS selectors, side by side.
Use the same row for both columns: the CSS and the XPath find the same element. Prefer the shortest locator that a developer is unlikely to change.
Find by attribute
| What you want | CSS | XPath |
|---|---|---|
| By id | #login | //*[@id='login'] |
| By class | .btn-primary | //*[contains(concat(' ', normalize-space(@class), ' '), ' btn-primary ')] |
| Tag and class | button.btn-primary | //button[contains(@class, 'btn-primary')] |
| By attribute | input[name='email'] | //input[@name='email'] |
| Test id | [data-testid='submit'] | //*[@data-testid='submit'] |
| Attribute starts with | a[href^='https'] | //a[starts-with(@href, 'https')] |
| Attribute ends with | img[src$='.png'] | //img[substring(@src, string-length(@src) - 3) = '.png'] |
| Attribute contains | a[href*='login'] | //a[contains(@href, 'login')] |
| Two attributes | input[type='text'][name='q'] | //input[@type='text' and @name='q'] |
| Either of two | input[name='q'], input[name='query'] | //input[@name='q' or @name='query'] |
Find by text and position
| What you want | CSS | XPath |
|---|---|---|
| Exact text | Not possible in plain CSS | //button[text()='Sign in'] |
| Text contains | Not possible in plain CSS | //a[contains(text(), 'Forgot')] |
| Text with extra spaces | Not possible in plain CSS | //span[normalize-space()='Total'] |
| First child | ul > li:first-child | //ul/li[1] |
| Last child | ul > li:last-child | //ul/li[last()] |
| Nth child | tr:nth-child(3) | //tr[3] |
| Nth of a type | p:nth-of-type(2) | (//p)[2] |
| Not this class | li:not(.disabled) | //li[not(contains(@class, 'disabled'))] |
Move around the page
| What you want | CSS | XPath |
|---|---|---|
| Direct child | form > input | //form/input |
| Any descendant | form input | //form//input |
| Parent | div:has(> input#email) | //input[@id='email']/.. |
| Ancestor | form:has(#email) | //input[@id='email']/ancestor::form |
| Next sibling | label + input | //label/following-sibling::input[1] |
| Any later sibling | h2 ~ p | //h2/following-sibling::p |
| Earlier sibling | Not possible in plain CSS | //input/preceding-sibling::label[1] |
| Input next to a label | Not possible in plain CSS | //label[text()='Email']/following-sibling::input |
| Row that has a cell with a class | tr:has(td.name) | //tr[td[contains(@class, 'name')]] |
| Third cell in the row for "Asha" | Not possible in plain CSS | //tr[td[text()='Asha']]/td[3] |
The same locator in code
| Tool | CSS | XPath |
|---|---|---|
| Selenium (Java) | driver.findElement(By.cssSelector("#login")) | driver.findElement(By.xpath("//*[@id='login']")) |
| Playwright (TS) | page.locator("#login") | page.locator("xpath=//*[@id='login']") |
Rules that keep locators stable
- Prefer, in order: a test id, a role with its accessible name, a label, a stable attribute. Use text and position last.
- Avoid absolute XPath like /html/body/div[2]/div[1]. Any layout change breaks it.
- Avoid generated class names such as css-1x2y3z. They change on every build.
- In Playwright, prefer page.getByRole("button", { name: "Sign in" }) and page.getByLabel("Email"). They read like what the user sees and survive markup changes.
- Check a locator in the browser console first: $$('css') and $x('xpath') in Chrome DevTools.
- :has() is supported by current browsers. On very old browsers, fall back to XPath for parent lookups.
Questions
Which is better, XPath or CSS selectors?
Use CSS when it can express the locator: it is shorter and easier to read. Use XPath when you need what CSS cannot do, such as matching by text or moving to an earlier sibling.
Can a CSS selector find an element by its text?
No. Plain CSS cannot match text. Use XPath with text() or normalize-space(), or Playwright's getByText and getByRole.
How do I select a parent element?
In XPath use /.. or the ancestor axis. In modern browsers CSS can use :has(), for example div:has(> input#email).
What is the difference between // and / in XPath?
A single slash selects a direct child. A double slash selects a descendant at any depth.
Get new cheatsheets by email
One email when a new cheatsheet or free resource goes up. Unsubscribe any time.