Advanced Patterns advanced svg namespaces icons

SVG, Namespaces and local-name()

Why //svg and //path return nothing in a browser, how namespaces work in the HTML DOM, and the local-name() idiom for icons, charts and icon buttons.

SVG, Namespaces and local-name()

Inline SVG is everywhere: icon buttons, charts, loaders, checkmarks. And //svg returns nothing. Neither does //path, //circle or //svg//*[@class='icon']. This is not a bug in your locator; it is XML namespaces.

What the HTML parser does

When the browser parses <svg> inside an HTML document, it creates elements in the SVG namespace (http://www.w3.org/2000/svg) rather than the HTML namespace. XPath name tests like svg or path match elements with no namespace, so they miss.

The same applies to MathML (<math>), and to any XML document with a default namespace.

The fix: local-name()

local-name() returns the element name without any namespace, so you can test it in a predicate:

//*[local-name()='svg']                          every inline svg
//*[local-name()='path']                         every path
//*[local-name()='svg'][@aria-label='Close']     an svg with a label
//*[local-name()='svg']//*[local-name()='circle']  circles inside svgs

name() also works for elements without a prefix, but local-name() is the safer habit because it ignores prefixes like svg:path in XML documents.

Icon buttons

The element you click is the button, not the svg. Anchor on the svg and go up, or use the button’s accessible name:

<button aria-label="Delete">
  <svg viewBox="0 0 24 24"><path d="..."/></svg>
</button>
//button[@aria-label='Delete']                                     best
//button[.//*[local-name()='svg'][@data-icon='trash']]              by icon data attribute
//*[local-name()='svg'][@data-icon='trash']/ancestor::button[1]     from the icon upward
//button[*[local-name()='svg']]                                    buttons that contain any svg

Icon libraries usually leave a stable hook: Font Awesome sets data-icon, Material adds a class or data-testid, Lucide sets class="lucide lucide-trash". Match on that rather than the path data.

Inside the svg

Attributes on SVG elements are not namespaced (unless prefixed), so @class, @id, @fill and @data-* work as normal once you have matched the element by local name:

//*[local-name()='svg'][@class='chart']//*[local-name()='rect'][@fill='#00D9FF']
//*[local-name()='g'][@data-series='revenue']/*[local-name()='rect']
//*[local-name()='text'][normalize-space()='Q3']

class on an SVG element is a plain attribute string, so contains(@class, ...) works like HTML.

Star ratings and repeated icons

<div class="rating" aria-label="4 out of 5 stars">
  <svg class="star filled"/><svg class="star filled"/><svg class="star filled"/><svg class="star filled"/><svg class="star"/>
</div>
count(//div[@class='rating']/*[local-name()='svg'][contains(@class, 'filled')])   4
(//div[@class='rating']/*[local-name()='svg'])[4]                                 the fourth star
//div[@class='rating'][@aria-label='4 out of 5 stars']                            better: assert the label

SVG used as a map or diagram

Clickable regions inside an SVG can be located like any element:

//*[local-name()='path'][@data-region='EU']
//*[local-name()='a'][contains(@href, '/region/eu')]    svg links are also namespaced

XML documents with namespaces

When scraping XML feeds or SOAP responses, the same idiom applies, and you can also test the namespace itself:

//*[local-name()='item']
//*[local-name()='price' and namespace-uri()='http://example.com/ns']

Tools like lxml let you register prefixes so you can write //ns:item instead. Browsers do not offer that through document.evaluate without a resolver function, which is why local-name() is the portable choice.

Playwright and CSS alternatives

CSS selectors ignore namespaces, so page.locator('button svg.icon') works where XPath needs local-name(). Inside a mostly-XPath codebase, local-name() keeps everything in one language; in Playwright you may prefer a role locator (getByRole('button', { name: 'Delete' })) and never touch the svg at all.

Try It Yourself

Open in Playground →

Compare with //svg on the same sample and confirm it returns zero matches.

Next Steps

  1. Node Types and Wildcards - The * wildcard this pattern relies on
  2. Shadow DOM and iframes - The other two things XPath cannot see through
  3. XPath in Playwright - When to hand off to role locators