XPath in the browser: document.evaluate

The DOM XPath API is older than querySelector but still the only way to walk upwards and to match by text, and its result types have real consequences.

document.evaluate

const expr = "//article[@data-id]/h2[contains(., 'Release')]";

const result = document.evaluate(
  expr,
  document,            // context node
  null,                // namespace resolver, null is fine for HTML
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  null                 // existing result to reuse, almost always null
);

const hits = [];
for (let i = 0; i < result.snapshotLength; i++) hits.push(result.snapshotItem(i));
console.log(hits.map(h => h.textContent.trim()));
XPathResult typeUse it whenCost
ANY_TYPEYou do not know the shapeYou must probe with resultType
NUMBER_TYPECount, string-lengthReturns .numberValue
STRING_TYPEstring(//h1)Returns .stringValue
BOOLEAN_TYPEExistence checksReturns .booleanValue
UNORDERED_NODE_ITERATOR_TYPEStreaming a huge match setInvalidated by DOM mutation
ORDERED_NODE_SNAPSHOT_TYPEYou want a stable arrayOne snapshot, then .snapshotItem(i)
⚠️
A live iterator is invalidated the moment you modify the DOM — the next iterateNext() throws with no useful message. If you are touching the document, always take ORDERED_NODE_SNAPSHOT_TYPE and copy the nodes out first.

Namespaces and the comparison with CSS

// XPath on an XML document needs a resolver function
const NS = { svg: "http://www.w3.org/2000/svg", xlink: "http://www.w3.org/1999/xlink" };
const resolver = (prefix) => NS[prefix] || null;

const svg = await fetch("/diagram.svg").then(r => r.text());
const xml = new DOMParser().parseFromString(svg, "image/svg+xml");

const first = xml.evaluate("//svg:rect[1]", xml, resolver,
                           XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
first.setAttribute("fill", "tomato");
  • querySelectorAll cannot select by text, cannot go to a parent or a preceding sibling, and cannot count.
  • XPath can do all three: //td[.="Total"]/parent::tr has no CSS equivalent.
  • XPath is slower than a native CSS engine on large documents, so prefer CSS when both work.
  • Comments and processing instructions are reachable only through XPath in the DOM API.
  • In an HTML document, tag and attribute names are matched case-insensitively by the HTML XPath implementation.
// Selecting the table row that contains a label, then walking up
const row = document.evaluate(
  "//td[normalize-space(.)='Total']/ancestor::tr[1]",
  document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
).singleNodeValue;
console.log(row.querySelector("td:last-child").textContent);

FAQ

Do I still need XPath in the browser for scraping?
Rarely. In a page you control, data attributes with querySelector are faster and easier to keep stable. XPath remains useful in userscripts, in tests against third-party markup, and for SVG or XML documents.
Why does count(//div) fail to compile?
Because the XPath parser in the browser is XPath 1.0 and takes namespace prefixes from your resolver. Check the prefix binding before suspecting the expression.

Robust XPath for scraping and testing CSS selectors, JSONPath and jq as alternatives

Last refreshed 2026-09-18.