Jim Nielsen’s Blog
Preferences
Theme: This feature requires JavaScript as well as the default site fidelity (see below).
Fidelity:

Controls the level of style and functionality of the site, a lower fidelity meaning less bandwidth, battery, and CPU usage. Learn more.

Multiple Inline SVGs (From QuickChart)

“multiple inline svg clip-path url id namespace collision quickchart.io”

👆 That is the keyword search for my problem. I’m not finding any results related specifically to this problem and using quickchart.io, so here’s my contribution to the internet and future person who has a similar keyword search.


I’m working on generating some stats and accompanying graphs for my blog (blog post to come on this…).

For the charts, I’m using the quickchart.io API (a tool I’ve used before) and it’s beautifully simple: pass data as a URL, get back a chart.

It’s working. My build hits the quickchart API, gets an SVG, and inlines it into my HTML. It looks great!

Until I try to add more than one SVG to the page. Then the charts look empty.

Which is weird, because they don’t look that way when I look at them in the file browser. Or at their own URLs on my local server.

Which, come to think of it, can only mean one thing: inlining them into the HTML is what’s causing the bug. But what’s the bug?

I open the developer tools and see the empty chart has a bunch of clip-path attributes pointing to <clipPath> definitions in the SVG using generic ID names (which are automatically generated by quickchart.io).

Ah-ha! QuickChart is generating SVGs with the same generic naming pattern for IDs (clip1, clip2, clip3, etc.). When multiple SVGs from QuickChart get embedded in the same document, they clobber each other’s namespace!

In other words: clip-path="url(#clip1)" is finding the first <clipPath id="clip1"> in the current document, which normally is its own SVG document. But this SVG document is embedded in an HTML document with other SVG documents using the same IDs, so they clash.

“Just reference the SVG as an <img> and it’ll work,” you might be thinking. But I need styling control over these SVGs, so <img> doesn’t work.

So I turn to the perenially-great, kiwi bird CSS tricks article on using SVG.

It looks like <object> is my best bet, as it’s the one that’ll still give me styling control. But I quickly realize its styling is local to the SVG itself, meaning it can’t inherit styles from the parent HTML document which is exactly what I want and need.

So now my options are: 1) duplicate the CSS/JS that controls the themeing in my parent HTML document so it works in each individual <object> SVG, or 2) find another solution.

I’ll find another solution.

I start to wonder, “When embedding an SVG in an HTML document, is there a way to scope IDs to the parent <svg> element?” I realize IDs must be unique per document but SVGs are documents too, right? But I guess they’re not when embedded in HTML.

I get it, what I’m asking for here is to have my cake and eat it too: I want the SVG to be part of the document when it’s convenient to me (to inherit the parent document’s styling) but not when it’s inconvenient for me (ID name collision).

It would be cool if you could scope ID naming, kind of like donut scope in CSS, to a specific area or parent. AFAIK something like this doesn’t exist (hit me up if you know something I don’t) but I kinda wish it did:

<html>
  <body>
    <h1>My Charts</h1>
    
    <h2>Generated Chart #1</h1>
    <!-- Set some attribute so ID names go past here -->
    <div scope-id="true">
      <!-- Embedded SVG. Example: -->
      <svg>
          <defs>
            <clipPath id="clip1">…</clipPath>
          </defs>
          <g clip-path="url(#clip1)">…</g>
        </svg>
    </div>
    
    <h2>Generated Chart #2</h1>
    <div scope-id="true">
      <!-- Embedded SVG. Example: -->
      <svg>
        <defs>
          <clipPath id="clip1">…</clipPath>
        </defs>
        <g clip-path="url(#clip1)">…</g>
      </svg>
    </div>
  </body>
</html>

The use case here seems like a pretty common one? Use a tool to generate multiple SVGs, then inline them into the same HTML document without clobbering each other. As a poster on StackOverlow put it:

SVGs are often pulled in from a variety of sources and for it to fall to the user to have to parse and re-pseudo-namespace everything to avoid potential clashes is pretty unfriendly.

Ultimately, what I need is multiple <svg> elements inlined to the same document for styling control. The pattern QuickChart uses for generating IDs is pretty straightforward (and from what I can tell, not configurable through the API), so I do what any reasonable person does when you need to tweak strings you have no control over: I reach for a regex.

/**
 * Take an SVG from QuickChart like this:
 * <svg>
 *   <defs>
 *     <clipPath id="clip1">…</clipPath>
 *   </defs>
 *   <g clip-path="url(#clip1)">…</g>
 * </svg>
 *
 * And make each ID unique by prepending a string:
 *
 * <svg>
 *   <defs>
 *     <clipPath id="my-unique-id__clip1">…</clipPath>
 *   </defs>
 *   <g clip-path="url(#my-unique-id__clip1)">…</g>
 * </svg>
 */
function uniquezSvg(svg, id) {
  return svg
    .replace(
      /id="clip/g,
      `id="${id}__clip`
    )
    .replace(
      /clip-path="url\(#clip/g,
      `clip-path="url(#${id}__clip`
    );
}

// Use it, e.g.
const svg = await fetchQuickChartSvg(…);
const newSvg = uniquezSvg(svg, "my-unique-id");

Boom, done. This rewrites the IDs for clip paths in SVGs generated by QuickChart so they can be embedded into the same HTML document.

This story brought to you by another day tinkering on my website.

Update 2022-09-18

@simevidas suggested on Twitter a parent-level ID prefix that all children would then inherit for their IDs, which I kind of like. Example:

<div id-prefix="chart-">
  <!-- Resulting id would be "chart-bar1" -->
  <svg id="bar1">…</svg>
    <!-- Resulting id would be "chart-bar2" -->
  <svg id="bar2">…</svg>
</div>

I’m not quite sure how you'd represent that in the DOM of the devtools, cause there’s a difference between what’s written in the original HTML and what the browser interprets it as – but I still kinda like the idea.