🍱 Lunchbox Hands

XPath Tester

Evaluate XPath expressions against XML or HTML live, with matched node types, namespaces, and copy-ready document.evaluate and Playwright snippets.

Paste XML or HTML, type an XPath expression, and see what it matches as you type. Node-set results list every match with its node type (element, attribute, text, comment, processing instruction), a short preview, and its absolute path. Number, string and boolean results show the value. Namespace declarations are picked up from the document, and you can add your own prefixes.

This is XPath 1.0, the version browsers implement, so it behaves like the selectors you run in Playwright, Selenium, Puppeteer or the DevTools console. XPath 2.0 and 3.1 functions such as matches() are not available. Everything runs in your browser and nothing is uploaded.

Namespaces

PrefixURISource
dchttp://purl.org/dc/elements/1.1/From document

Prefixes you add here override the ones declared in the document.

Copy as code

document.evaluate
const result = document.evaluate('//book[price > 10]/title', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
for (let i = 0; i < result.snapshotLength; i++) {
  console.log(result.snapshotItem(i))
}
Playwright
page.locator('xpath=//book[price > 10]/title')
Selenium (Python)
driver.find_elements(By.XPATH, "//book[price > 10]/title")
Puppeteer
await page.$$('::-p-xpath(//book[price > 10]/title)')

page.$x() was removed in Puppeteer 22; this uses the ::-p-xpath selector instead.

XPath cheat sheet

ExpressionWhat it selects
//tagEvery tag element anywhere in the document.
/root/childchild elements directly under the root element named root.
//tag[@attr='v']tag elements whose attr attribute is exactly v.
//tag[contains(@class,'x')]tag elements whose class contains the substring x. Note that this also matches xy.
//tag[text()='x']tag elements with a direct text node equal to x. Fails if the text is split by child elements or has extra spaces.
//tag[normalize-space()='x']tag elements whose full text, with whitespace trimmed and collapsed, is x. Usually what you want for buttons and links.
//tag[1]Every tag that is the first tag child of its parent. Can return many nodes.
(//tag)[1]The first tag in the whole document. Always zero or one node.
//tag[last()]Every tag that is the last tag child of its parent.
//tag/..The parent of every tag element.
//tag/@hrefThe href attribute nodes themselves, not the elements.
//*[local-name()='x']Elements named x in any namespace. The usual fix for default namespaces.
//a[starts-with(@href,'https')]Links whose href begins with https.

The pair that trips people up is //tag[1] and (//tag)[1]. In XPath a predicate belongs to the step it follows. //li[1] expands to /descendant-or-self::node()/child::li[1]: for each node in the document, take its first li child. Every list contributes one item. Parentheses turn the path into a single node-set first, so (//li)[1] filters that whole set and returns just the first li in document order. The tester shows a hint when it sees the first form return more than one node.

Namespaces and the empty result

Feeds, SOAP responses, SVG, sitemaps and Office files nearly always declare a namespace. When the declaration is a default one, xmlns="..." with no prefix, every element under it belongs to that namespace even though no prefix appears in the markup. XPath 1.0 cannot express a default namespace, so plain names stop matching. The tester reads the declarations from your document, lists them in the namespace table, and resolves any prefix you use. Add a row such as s mapped to http://www.sitemaps.org/schemas/sitemap/0.9 and //s:loc works. Or skip prefixes and use //*[local-name()='loc'].

The path shown for each match is built from the document root with a sibling index wherever two siblings share a name. In XML mode, elements in a default namespace are written with local-name() so the path evaluates when you click it.

Limits worth knowing

Only XPath 1.0 is supported, because that is what document.evaluate provides in every browser. There is no matches(), ends-with(), string-join(), lower-case() or sequence syntax. HTML mode uses the browser's HTML parser, so the tree you query can differ from your source: a tbody appears inside tables, stray tags get closed, and head and body are added. The results table shows the first 500 matches.

Related tools

Frequently asked questions

Why does //book match nothing when my XML clearly has book elements?

Almost always a default namespace. If the root says xmlns="http://example.com/ns", every unprefixed element below it lives in that namespace. XPath 1.0 has no idea of a default namespace, so the name test book only matches a book element in no namespace at all. You have two fixes. Match on local name with //*[local-name()='book'], or bind a prefix to the namespace URI (this tool lets you add one in the namespace table) and write //b:book. The tester spots this case and offers the local-name() rewrite when a node-set comes back empty.

What is the difference between //tag[1] and (//tag)[1]?

The predicate binds to the step, not to the whole path. //li[1] is short for /descendant-or-self::node()/child::li[1], which means every li that is the first li child of its own parent. With three lists on a page, that returns three nodes. (//li)[1] first collects every li in the document into one node-set in document order, then takes the first of that set, so it returns exactly one node. Use the parenthesised form whenever you mean "the first match on the page". The same applies to [last()] and any other position.

XPath or CSS selectors: which should I use?

CSS selectors are shorter, faster to read, and the default in most test tools. They win for ids, classes, attributes and simple structure. XPath wins when you need something CSS cannot express: matching on text content (//button[normalize-space()='Save']), walking up to a parent or ancestor (//span[@class='price']/..), selecting attributes or text nodes themselves, comparing numbers ([price > 10]), or querying XML with namespaces. A good rule is CSS first, then XPath when you would otherwise need JavaScript to filter the results.

Does HTML mode need lowercase tag names?

Write them in lowercase. HTML mode parses your markup the way a browser does, which means it fixes broken markup, adds missing html, head and body elements, and stores element and attribute names in lowercase. HTML elements also sit in the XHTML namespace, but browsers special-case HTML documents so that //div matches without a prefix. Attribute values and text are still case-sensitive, so //a[@class='Nav'] will not match class="nav". If you want exact, case-preserving parsing, switch to XML mode, which needs well-formed input.

Why do matches(), ends-with() and string-join() give an error?

Browsers only implement XPath 1.0, and those are XPath 2.0 or 3.1 functions. In 1.0 you have contains(), starts-with(), substring(), substring-before(), substring-after(), normalize-space(), translate(), string-length(), count(), sum(), position() and last(). You can fake ends-with(@href, '.pdf') with substring(@href, string-length(@href) - 3) = '.pdf', and case-insensitive matching with translate(). Regular expressions have no 1.0 equivalent. If you need XPath 2.0 or later, use a server-side processor such as Saxon.

Is my document uploaded anywhere?

No. The document is parsed with your browser's own DOMParser and the expression runs through the browser's built-in document.evaluate, all on this page. Neither your XML or HTML nor your expressions are sent to a server. The only request the page makes is an anonymous usage count that records that the tool was used, with no content attached. You can disconnect from the network after the page loads and the tester keeps working.

Get weekly dev tools and tips