XPath Syntax Reference
Browsers implement XPath 1.0 through document.evaluate, and nothing later. That is the version this page documents, because it is the one Chrome, Firefox, Safari, Selenium and Playwright actually run. Anything marked 2.0 or above will throw wherever you try to use it in a browser.
Abbreviated syntax
| Syntax | Full form | Meaning |
|---|---|---|
| // | /descendant-or-self::node()/ | Any descendant at any depth. At the start of an expression it searches the whole document. |
| / | /child:: | One step down to a direct child. A leading / anchors at the document root. |
| . | self::node() | The context node. |
| .. | parent::node() | The parent of the context node. |
| @ | attribute:: | An attribute of the context node. |
| * | child::* | Any element child. Written after @ it means any attribute. |
| node() | child::node() | Any node: element, text, comment or processing instruction. |
| text() | child::text() | Text node children only. It does not reach text inside descendants. |
| [n] | [position() = n] | The nth node of that step, counting from 1. |
Axes
| Axis | Selects | CSS equivalent |
|---|---|---|
| child | Direct children of the context node. | > (child combinator) |
| descendant | Every node below, at any depth. Not the node itself. | space (descendant combinator) |
| descendant-or-self | The context node plus every descendant. | none |
| parent | The single parent node. | none |
| ancestor | Every ancestor up to the document root. | none |
| following-sibling | Siblings after this node, in document order. | ~ (and + for the first one) |
| preceding-sibling | Siblings before this node, nearest first. | none |
| following | Every node after this one in document order, excluding its descendants. | none |
| preceding | Every node before this one, excluding its ancestors. | none |
| self | The context node itself. | :scope, inside a scoped query |
| attribute | Attributes of the context node. | none (CSS matches on attributes but cannot select them) |
An axis decides which direction a step walks. CSS only has four of them, which is why the others are the reason to reach for XPath at all.
XPath 1.0 functions
| Function | Returns | Example |
|---|---|---|
| count() | number | count(//li) |
| position() | number | //li[position() = 2] |
| last() | number | //li[last()] |
| contains() | boolean | //p[contains(., 'total')] |
| starts-with() | boolean | //a[starts-with(@href, '/docs')] |
| substring() | string | substring('abcdef', 2, 3) is 'bcd' |
| string-length() | number | //p[string-length(normalize-space(.)) > 0] |
| normalize-space() | string | normalize-space(//h1) |
| concat() | string | concat(' ', @class, ' ') |
| translate() | string | translate(@class, 'ABC', 'abc') |
| not() | boolean | //div[not(@hidden)] |
| local-name() | string | //*[local-name() = 'entry'] |
| namespace-uri() | string | //*[namespace-uri() = 'http://www.w3.org/2005/Atom'] |
| name() | string | //*[name() = 'atom:entry'] |
| string() | string | string(//title) |
| number() | number | number(//price) |
| boolean() | boolean | boolean(//zzz) is false |
Not in XPath 1.0
| Construct | Version | Note |
|---|---|---|
| matches() | 2.0 | Regular-expression matching. Use contains(), starts-with() or translate() instead. |
| ends-with() | 2.0 | Rewrite as substring(@a, string-length(@a) - N + 1) = 'value'. |
| for | 2.0 | Range and iteration expressions. No browser equivalent. |
| let | 3.0 | Variable binding inside an expression. No browser equivalent. |
| sequence types | 2.0 | item(), xs:string and the rest of the type system. XPath 1.0 has four types: node-set, string, number, boolean. |
| tokenize() | 2.0 | Splitting a string into a sequence. Do the split outside the expression. |
These appear constantly in answers written for XSLT processors or Java libraries. None of them exists in a browser, and each one throws an invalid-expression error.
Namespace recipes
| Problem | Expression |
|---|---|
| The document declares a default xmlns, so //entry matches nothing | //*[local-name()='entry'] |
| Match the local name and the namespace precisely | //*[local-name()='entry' and namespace-uri()='http://www.w3.org/2005/Atom'] |
| Match an element carrying a namespaced attribute | //*[@*[local-name()='href' and namespace-uri()='http://www.w3.org/1999/xlink']] |
XPath 1.0 has no default namespace. An unprefixed name test only matches elements in no namespace, which is why //entry returns nothing on an Atom feed. Write the attribute form as a nested predicate: the flat version [local-name(@*)='href' and namespace-uri(@*)='...'] is wrong, because each function is evaluated against the attribute set independently and the two conditions can end up describing different attributes.
Frequently asked questions
Which XPath version do browsers support?
XPath 1.0 only, exposed as document.evaluate. Chromium, Gecko and WebKit all stopped there, and Selenium and Playwright delegate to the browser, so they inherit the same limit. If an expression needs matches() or ends-with(), it needs a JavaScript XPath library, not a browser.
Why does //entry return nothing on an Atom or SOAP document?
Because the document declares a default namespace and XPath 1.0 has no concept of one. An unprefixed name test matches only elements in no namespace, so it skips every element in the feed. Either bind a prefix through the resolver and write //atom:entry, or match on local-name() as in the namespace recipes above.
What is the difference between contains(., 'x') and contains(text(), 'x')?
The dot is the element's whole string value, including text inside descendants. text() is the set of text node children, and contains() converts a node-set by taking its first node, so contains(text(), 'x') only ever looks at the first text node. On <p>Hello <b>world</b></p> the first form sees 'Hello world' and the second sees 'Hello '.