Guide
Converting CSS selectors to XPath for scrapers and test tools
Browsers, developer tools and front-end developers think in CSS selectors, but many scraping libraries and test frameworks were built around XPath: Selenium locators, Scrapy and lxml, and XSLT all expect it, and XPath can do things CSS cannot, such as matching text or stepping up to a parent. This converter translates a CSS selector into an equivalent XPath 1.0 expression.
Building blocks
Each compound selector becomes one step of an XPath path. A type selector names the element, so div becomes div, and a missing type becomes *. An ID becomes the predicate [@id='main']. Attribute selectors become predicates too: [href] checks that the attribute exists, [type="email"] compares its value, ^= becomes starts-with(), *= becomes contains() and $= is written with substring() because XPath 1.0 has no ends-with function.
Matching classes properly
A class selector is the part most hand-written XPath gets wrong. contains(@class, 'note') also matches notes and footnote. The converter uses the standard idiom contains(concat(' ', normalize-space(@class), ' '), ' note '), which pads the attribute with spaces so the class matches only as a whole word, exactly like .note in CSS. The ~= attribute operator uses the same technique.
Combinators become axes
The descendant combinator, a space, becomes //, and the child combinator > becomes /. The adjacent sibling combinator + becomes /following-sibling::*[1]/self::p, which takes the very next element sibling and checks its type; the general sibling combinator ~ becomes /following-sibling::. A comma-separated selector list becomes a union of paths joined with |.
Pseudo-classes
Structural pseudo-classes translate into predicates about siblings: :first-child becomes not(preceding-sibling::*), :last-child becomes not(following-sibling::*) and :nth-child(3) counts preceding siblings. :nth-of-type(2) becomes the positional predicate [2]. :empty, :checked and :disabled check content and attributes, and :not() with a simple selector inside becomes not(self::…).
A worked example
From the Sample, div#main > p.note becomes //div[@id='main']/p[contains(concat(' ', normalize-space(@class), ' '), ' note ')]. ul.menu li:first-child a[href^="https"] becomes //ul[contains(concat(' ', normalize-space(@class), ' '), ' menu ')]//li[not(preceding-sibling::*)]//a[starts-with(@href, 'https')], and form input[type="email"]:not([disabled]) becomes //form//input[@type='email'][not(self::*[@disabled])]. The sibling example h2 + p turns into //h2/following-sibling::*[1]/self::p.
What cannot be converted
XPath sees the document tree only, so anything about state or rendering has no equivalent: :hover, :focus, :visited, :target and pseudo-elements such as ::before produce a clear error instead of a wrong expression. :nth-child() accepts a plain number; formulas such as 2n+1 need to be written by hand with count(preceding-sibling::*) mod 2.
Using the result
Paste the expression into document.evaluate in the browser console, By.xpath in Selenium, page.locator('xpath=…') in Playwright or response.xpath() in Scrapy. Test it first against a saved copy of the page with the XPath Tester to make sure it matches what you expect.
Why convert instead of writing XPath by hand
CSS selectors copied from developer tools are usually short and already tested in the browser, while hand-written XPath tends to grow long and brittle, with absolute paths such as /html/body/div[3]/div[2]/p that break when the page layout changes. Converting keeps the intent of the selector, an ID here, a class there, and produces an expression that survives small layout changes. When you need the extras XPath offers, start from the converted expression and add a text predicate or a parent step to it rather than starting from scratch.
Performance notes
Expressions beginning with // search the whole document. That is fine for test locators and single pages, but in large XML files anchor the start where you can, for example //section[@id='prices']//td, so the engine narrows the search early.
Related tools
- XPath Tester evaluates the expression against real XML or HTML.
- CSS Specificity Calculator scores the original selectors.
- Regex Tester helps with text patterns inside predicates.
- XML Formatter tidies documents before you query them.
Checked against the current version of the CSS to XPath on .

