WW Tools
All references

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

SyntaxFull formMeaning
///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

AxisSelectsCSS equivalent
childDirect children of the context node.> (child combinator)
descendantEvery node below, at any depth. Not the node itself.space (descendant combinator)
descendant-or-selfThe context node plus every descendant.none
parentThe single parent node.none
ancestorEvery ancestor up to the document root.none
following-siblingSiblings after this node, in document order.~ (and + for the first one)
preceding-siblingSiblings before this node, nearest first.none
followingEvery node after this one in document order, excluding its descendants.none
precedingEvery node before this one, excluding its ancestors.none
selfThe context node itself.:scope, inside a scoped query
attributeAttributes 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

FunctionReturnsExample
count()numbercount(//li)
position()number//li[position() = 2]
last()number//li[last()]
contains()boolean//p[contains(., 'total')]
starts-with()boolean//a[starts-with(@href, '/docs')]
substring()stringsubstring('abcdef', 2, 3) is 'bcd'
string-length()number//p[string-length(normalize-space(.)) > 0]
normalize-space()stringnormalize-space(//h1)
concat()stringconcat(' ', @class, ' ')
translate()stringtranslate(@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()stringstring(//title)
number()numbernumber(//price)
boolean()booleanboolean(//zzz) is false

Not in XPath 1.0

ConstructVersionNote
matches()2.0Regular-expression matching. Use contains(), starts-with() or translate() instead.
ends-with()2.0Rewrite as substring(@a, string-length(@a) - N + 1) = 'value'.
for2.0Range and iteration expressions. No browser equivalent.
let3.0Variable binding inside an expression. No browser equivalent.
sequence types2.0item(), xs:string and the rest of the type system. XPath 1.0 has four types: node-set, string, number, boolean.
tokenize()2.0Splitting 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

ProblemExpression
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 '.

Use the tool