Skip to main content

How to Embed an SVG as a Data URI in CSS and HTML

Inline a small SVG icon as a data URI in CSS or HTML, URL-encode it instead of base64 to save bytes, and drop one HTTP request per icon.

Published By Li Lei
#svg #data-uri #css #performance #frontend

How to Embed an SVG as a Data URI in CSS and HTML

A data URI lets you write the contents of a file directly into the place that would normally point at it. Instead of background-image: url(icon.svg), which sends the browser off to fetch a separate file, you write background-image: url("data:image/svg+xml,<your markup here>") and the icon ships inside the stylesheet. No second file, no extra round trip, no flash of a missing image on first paint.

For small vector icons this is one of the few front-end wins that costs nothing and breaks nothing. The catch is that everyone reaches for base64 out of habit, and for SVG that is usually the wrong encoding. This post walks through how the embed actually works, why URL-encoding beats base64 for text-based SVG, and where the line sits between a smart inline and a regretful one.

What a data URI actually is

A data URI has a simple shape: data:[mediatype][;base64],<data>. For SVG the media type is image/svg+xml, and the comma separates the header from the payload. Two forms are valid:

  • Base64: data:image/svg+xml;base64,PHN2ZyB4bWxucz0i... — the ;base64 token tells the browser the payload is base64 and must be decoded back to bytes.
  • URL-encoded (plain text): data:image/svg+xml,%3Csvg%20xmlns... — no ;base64, so the browser reads the payload as literal text with percent-escapes.

The second form is the one most people forget exists. SVG is text — angle brackets, attribute names, path letters, digits — so you can drop the raw markup straight into the URI and only escape the handful of characters that would otherwise confuse the parser.

Why URL-encoding beats base64 for SVG

Base64 is a fixed tax. It mechanically turns every 3 bytes of input into 4 ASCII characters, which is a flat ~33% size penalty no matter what the content is. That made sense for binary blobs like PNGs, where the bytes are not text to begin with. SVG is already almost entirely ASCII, so base64-ing it means inflating perfectly readable text by a third for no reason.

URL-encoding does the opposite. It leaves everything that is safe inside url(...) untouched and escapes only the characters that genuinely break it:

  • < and > — the XML tag delimiters
  • # — which starts a URL fragment and would truncate the image (more on this below)
  • % — the escape character itself, so a literal one becomes %25
  • double quotes, when you wrap the value in url("...")

For a typical icon you end up escaping maybe 5–10% of the bytes instead of inflating 100% of them. In practice URL-encoding lands 20–40% smaller than base64 for the same SVG. A 600-byte icon that base64s to ~820 bytes might URL-encode to ~640 — and you can still read the markup in DevTools, which you cannot do with a base64 blob.

The # rule is the one that bites people. Inside a data URI, # begins the fragment identifier, so an unescaped fill="#ff0000" silently chops the image off at the hash. It has to become %23. If you ever paste a hand-written data URI and it renders blank, an unescaped # is the first thing to check.

A worked example: an icon as a CSS background

Here is a small chevron icon, 24×24, the kind you'd use on a dropdown:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>

Base64-encoded, the CSS rule looks like this — opaque and unreadable:

.dropdown::after {
  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzMzIiBzdHJva2Utd2lkdGg9IjIiPjxwYXRoIGQ9Ik02IDlsNiA2IDYtNiIvPjwvc3ZnPg==");
}

URL-encoded, it stays legible and comes out shorter:

.dropdown::after {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23333'%20stroke-width='2'%3E%3Cpath%20d='M6%209l6%206%206-6'/%3E%3C/svg%3E");
}

Notice stroke="#333" became stroke='%23333' — the # is escaped, and the inner attribute quotes were switched to single quotes so they don't collide with the outer url("...") double quotes. That icon now renders on first paint with zero network requests, and your browser's network panel shows one fewer line. Across a component library with a couple dozen icons, those saved requests add up fast — especially on slow connections where each round trip has a fixed latency cost.

One thing the inline <svg> in your HTML gets for free but the data URI does not: a data URI loaded through <img> or CSS url() is parsed as a standalone document, so it must carry xmlns="http://www.w3.org/2000/svg". Leave it off and the icon simply won't render. The example above includes it on purpose.

The size tradeoff vs an external file

Inlining is not free even when it's small. The bytes you bake into your CSS or HTML can never be cached separately from the file that holds them. An external icon.svg is fetched once and reused on every page; an inlined one re-ships inside every page's CSS that references it. So the math is: you trade one cacheable network request for repeated bytes in your bundle.

That trade pays off for small, frequently-used assets and loses for large ones. The practical sweet spot is anything under roughly 4–8 KB encoded — icons, logos, simple decorative shapes. A 50 KB illustration inlined into your global stylesheet is a mistake: it bloats every page load and you've thrown away the caching win for nothing. Link big assets as external files and reserve inlining for the small stuff.

If you're chasing the smallest possible inline size, optimize before you encode. Running the markup through svg-optimizer to strip comments, collapse whitespace, and trim redundant attributes can shave a meaningful chunk off the bytes you're about to bake in — and a smaller source means a smaller data URI either way you encode it.

Doing it without hand-counting bytes

I spent an embarrassing afternoon early on manually percent-escaping an SVG by hand for a CSS background and getting it wrong twice — once an unescaped # truncated the image, once a stray double quote terminated the url() string early. That's exactly the kind of fiddly, error-prone work a tool should do for you. The SVG to Data URI converter takes your markup or a dropped .svg file, produces both the URL-encoded and base64 forms at once, and shows a byte-for-byte size diff so you can see which one actually wins for your specific file. It escapes exactly the characters that need escaping — nothing more — and hands you three copy-ready outputs: a full CSS rule, an <img> tag, or the bare URI for a CSS custom property like --icon-check. Everything runs in your browser tab; the SVG never leaves the device.

A couple of decisions it makes for you that are worth knowing. It defaults to URL-encoding because that's the smaller, readable choice for almost every SVG — only switch to base64 when a consumer specifically can't handle a URL-encoded payload. And it wraps the value in url("...") with double quotes, escaping any double quotes inside the SVG so they can't end the string early.

When inlining isn't the answer

Data URIs are a sharp tool for a narrow job. They shine for small icons, logo marks in HTML email (modern webmail clients render SVG fine, though Outlook desktop does not — for that audience, rasterize first), and reusable CSS variables full of vector art. They're the wrong call for large illustrations, anything you want cached independently, or formats that aren't already text.

If your "icon" is really a decorative pattern, you may not need a data URI at all — css-pattern-generator can produce repeating backgrounds in pure CSS that skip the encoding step entirely. And if you're working with raster formats rather than vectors, base64-image-converter covers PNG and JPEG, where base64 genuinely is the right encoding because the bytes were never text in the first place.

Pick the smallest encoding, keep inlining to small assets, and let a tool handle the escaping. Your CSS stays readable, your network panel stays quiet, and every page load carries a few fewer bytes.


Made by Toolora · Updated 2026-06-13