1. Home
  2. Developer tools
  3. CSS to XPath

CSS to XPath

Use the selector you already know from DevTools in a scraper or test framework that wants XPath.

Free, no sign-up. Runs in your browser: nothing is uploaded.

How to use CSS to XPath

  1. Paste one CSS selector per line.
  2. Copy the XPath.
  3. Check it against your page with the XPath Tester.

Features

  • Descendant, child (>), adjacent (+) and sibling (~) combinators
  • Whole-word class matching with contains(concat(…))
  • Attribute operators =, ^=, $=, *=, ~= and |=
  • :first-child, :last-child, :nth-child(n), :nth-of-type(n), :not() and :empty
  • Selector lists become XPath unions

Supported formats

Input

  • Typed or pasted text

You get

  • Copied text

Private by design

Everything you enter is processed by your browser on this device. Nothing is sent to openWorkstand or anyone else, so the CSS to XPath is safe for private and work data. After you have used it once while online, it also works without an internet connection. How local processing works

Good to know

  • State pseudo-classes (:hover, :focus) and pseudo-elements have no XPath equivalent.
  • :nth-child() supports plain numbers, not an+b formulas.

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.

Checked against the current version of the CSS to XPath on .

Frequently asked questions

Is XPath slower than CSS selectors in browsers?

For typical test and scraping workloads the difference is negligible; both are evaluated natively. CSS selectors are often a little faster and easier to read, so use them where a tool accepts both, and XPath when you need text matching or moving up to a parent.

How do I select an element by its text with XPath?

Use contains(., "text") or normalize-space(.) = "exact text" in a predicate, for example //button[normalize-space(.) = "Save"]. The converter writes :contains("text") this way, a pseudo-class some libraries add to CSS but browsers do not support.

Why not just use contains(@class, "note")?

Because it also matches class="notes" or "footnote". Wrapping the attribute in spaces with concat and normalize-space matches the class as a whole word, which is what the CSS .note selector means.

Which pseudo-classes cannot be converted?

Anything about state or rendering, such as :hover, :focus, :visited, :checked set by the user, and pseudo-elements like ::before. XPath only sees the document tree.

Keep working with the result

openWorkstand also includes a free workspace with documents, spreadsheets, tasks, projects and a calendar that save to a folder on your computer.