XPath vs CSS selector — locator translator

Type a locator in either language below. You get the equivalent in the other, and both are evaluated against the same editable document so you can see they match the same nodes — or exactly where they diverge. article.card > h2 becomes a path; //h2/parent::article comes back with the reason CSS can't express it. Everything runs in your browser.

Example

XPath on the XHTML Page sample:

//article[contains(concat(' ', normalize-space(@class), ' '), ' card ')]/h2

The XPath equivalent of the CSS selector article.card > h2 — note the class test matches a whole token, which is what .card means.

Open it in the Sandbox

The equivalences, side by side

These are the translations the tool above performs. The class row is the one people usually get wrong by hand: .card matches a *token* in the class attribute, so @class = 'card' is not the same thing — an element with class="card featured" matches the CSS but not the naive XPath.

CSS selectors and their XPath equivalents
GoalCSSXPath
Element anywherediv//div
By id#main-nav//*[@id = 'main-nav']
By class.card//*[contains(concat(' ', normalize-space(@class), ' '), ' card ')]
Attribute existsa[href]//a[@href]
Attribute equalsa[rel="me"]//a[@rel = 'me']
Attribute starts witha[href^="https"]//a[starts-with(@href, 'https')]
Attribute ends witha[href$=".pdf"]//a[ends-with(@href, '.pdf')]
Attribute containsa[href*="blog"]//a[contains(@href, 'blog')]
Descendantnav a//nav//a
Direct childul > li//ul/li
Adjacent siblingh2 + p//h2/following-sibling::p[1]
General siblingh2 ~ p//h2/following-sibling::p
First childli:first-child//li[not(preceding-sibling::*)]
Nth childli:nth-child(3)//li[count(preceding-sibling::*) = 2]
Nth of typeli:nth-of-type(3)//li[3]
Unionh1, h2//h1 | //h2

What only XPath can do

This is the reason XPath survives in scraping and test automation despite CSS being shorter for the easy cases. None of the following has a CSS selector equivalent — the translator reports them rather than approximating them.

  • Walk upward: parent::, ancestor:: — find the row that contains a cell, the card that contains a heading.
  • Look backward: preceding-sibling::, preceding:: — read the label that comes *before* a value.
  • Match on text content: //a[normalize-space() = 'Next'], //p[contains(., 'total')].
  • Count and compare: //tr[count(td) > 3], //section[not(p)].
  • Select non-element nodes: attributes (//a/@href), text nodes, comments.
  • Compute: string functions, arithmetic, and in XPath 3.1 sequences, maps and arrays.

What CSS does better

For the common case — an element with a class, inside another element — CSS is shorter, more readable, and reviewers understand it at a glance. It is also the browser's native selector language, so it runs at full speed in querySelectorAll with no engine to load.

CSS has no version problem either. XPath in the browser and in Selenium is 1.0: no ends-with, no sequences, no let. A selector that works in one browser works in all of them, which is not something you can assume about an XPath expression written against a 3.1 engine.

Which should I use — scraping or UI tests?

For scraping, start with CSS and reach for XPath the moment the data you want is defined by its relationship to something else — the price in the row whose first cell says "Total", the link after a given heading. That relationship is exactly what CSS cannot express.

For UI tests, prefer whatever is most stable: a data-testid in CSS beats a clever XPath every time. XPath earns its place when the only stable anchor is visible text, because a locator built on text survives a markup refactor that renames every class.

One caveat: namespaces

querySelectorAll largely ignores namespaces, while XPath does not. Real XHTML declares xmlns="http://www.w3.org/1999/xhtml", and against that document //div matches nothing — you need a bound prefix, //xhtml:div. The example document above is namespace-free on purpose so the two locators stay directly comparable; the Sandbox detects namespaces and offers you the prefixes when your own document has them.

Keep going