The Truth(tm) about encoding SVG in data URIs

February 9th, 2024. Tagged: CSS, images


tl;dr: You can stop worrying and URL-encode only the # character.

What?

So you want to have an SVG image in a CSS stylesheet. Yup, using data URIs (hey lookie, a 2009 post). There are a number of reasons not to embed images in CSS to begin with (caching, reuse), but hey, sometimes you're not in a position to make that particular call.

Base64-encode

One way to go about it is to base64-encode the SVG, like:

background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=')

Drawbacks: Base64 makes the content larger by 25-30%. And also a human reading the code cannot tell what's in the image. (A nice feature of SVG is being able to tell what's in an image, roughly)

URL-encode

Another way to include an SVG is to use URL encoding:

background: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%2F%3E')

Drawback: the image payload is even larger. All these %20 (spaces) and %3C (<) characters quickly add up. The SVG is a bit more readable though.

As-is

A quick bit of testing in modern browsers suggests that the browser can understand the unencoded SVG perfectly well, so how about a new solution: SVG as-is.

background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"></svg>')

It all works fine for this particular type of MVP SVG but, as I quickly found out, as soon as you add a color like fill="#bad" to the SVG, the # acts like a URL hash and everything after it is no longer part of the SVG. So the image appears broken.

Selective URL-encoding

I was curious what other characters may brake the image and I looked and I asked around. In the webperf slack, Radu pointed to Sass/Bootstrap that's been around forever, it's battle-tested, (m)old-browser-verified and so on. Sass escapes these characters:

< becomes %3c
> becomes %3e
# becomes %23
( becomes %28
) becomes %29

I see the #hash is there, but the other characters? Digging through github I saw that the encoding was added 5 years ago containing initially only the characters <>#. The parentheses were added later to avoid a bug in a CSS minifier.

If we use a decent CSS minifier, we can forget about )( and we're left with <>#. I cannot find a reputable source for <> but the rumor has it it's for IE support. Well, IE is no more. We're left with only # to worry about.

# encoding

And here's the conclusion: in this day and age, encode your # (replace with %28) and enjoy small payloads, readable SVGs and modern browser support.

Future-proof?

The only nagging thing is that MDN will tell you to URL-encode. That's the right way. The fact that browsers are tolerant may be only temporary. But, if browsers suddenly decide to be strict, that'd be a Web-breaking change. Because a zillion web pages have partially encoded SVGs, I mean Sass/Bootstrap is popular. And browsers go to great lengths to avoid breaking the web. So I think it's safe to assume this minimal #-encoding will work for a looong time.

p.s. And, of course, escape the quotes you use in the url(). I'd suggest using single quotes, so the SVG itself can use double quotes. All of these should be ok though:

background: url('data:image/svg+xml,<svg xmlns=\'http://www.w3.org/2000/svg\'></svg>');
background: url("data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>");
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"></svg>');
background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>");

p.p.s. Apologies about the cheesy "the Truth" in the title. I'm aiming to be a tad obnoxious to trick people into proving me wrong. The Truth is what I'm seeking even if I'm wrong temporarily.

Comments? Find me on BlueSky, Mastodon, LinkedIn, Threads, Twitter