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 type | Use it when | Cost |
|---|---|---|
| ANY_TYPE | You do not know the shape | You must probe with resultType |
| NUMBER_TYPE | Count, string-length | Returns .numberValue |
| STRING_TYPE | string(//h1) | Returns .stringValue |
| BOOLEAN_TYPE | Existence checks | Returns .booleanValue |
| UNORDERED_NODE_ITERATOR_TYPE | Streaming a huge match set | Invalidated by DOM mutation |
| ORDERED_NODE_SNAPSHOT_TYPE | You want a stable array | One 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");querySelectorAllcannot select by text, cannot go to a parent or a preceding sibling, and cannot count.- XPath can do all three:
//td[.="Total"]/parent::trhas 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.
Related
Robust XPath for scraping and testing CSS selectors, JSONPath and jq as alternatives
Last refreshed 2026-09-18.