SVG Data URIs: The Right Way to Inline SVG in CSS
Embedding an SVG directly into a CSS file or HTML attribute as a data URI is one of the great unsung performance wins of the modern web — saved HTTP request, no caching headache, instantly themeable from CSS variables. It is also, for almost everyone who tries it for the first time, an immediate descent into encoding confusion. This article explains the actual choices, the byte cost of each, and the right answer in 2026.
What a data URI actually is
A data URI is a URL that contains its own content, encoded inline. The format is simple:
data:[<media type>][;base64],<data>For an SVG, the media type is image/svg+xml and the data is the SVG markup itself, optionally encoded. The browser treats the URI as if it had been fetched from a network URL, but without the network round-trip — perfect for small, frequently-used assets where a separate HTTP request would cost more than the payload itself.
The two encoding modes
You have to encode the data because URLs do not allow arbitrary characters — quotes, hashes, angle brackets, line breaks, and a handful of others all have special meaning in CSS or HTML attribute contexts and must be escaped. Two encoding strategies dominate:
- Base64. Convert the bytes to a 64-character ASCII alphabet. Very safe (no character ever needs escaping again), universally supported, but always 33% larger than the input by definition.
- URL-encoding. Replace only the characters that actually conflict (
#→%23,<→%3C, etc.). For SVG specifically this can be done either fully (escape every reserved character) or minimally (escape only the absolutely essential ones). The size cost depends on how aggressive you are.
The base64 myth
You will see base64 SVG data URIs everywhere. Most online “SVG to CSS” converters produce base64 output by default. This is a historical artefact, not a recommendation: base64 is the right encoding for binary data (PNG, JPG, fonts) where there’s no alternative. SVG is text. Base64-encoding text is wasteful both in file size and in gzip-compression efficiency.
The size cost is mathematical: base64 represents three bytes of input as four bytes of output, a deterministic 33% increase plus a tiny amount of padding. A 1 KB SVG becomes 1.33 KB base64-encoded before you’ve even added the data:image/svg+xml;base64, prefix.
The compression cost is sneakier: base64-encoded data does not compress well because the encoding scrambles patterns. Raw SVG contains a lot of repeated substrings (“fill="#”, “stroke="”, path command letters) which gzip and brotli love. Base64 destroys that structure, so the compressed CSS file ends up significantly larger than if you’d shipped URL-encoded SVG.
Combined verdict: base64 SVG data URIs are essentially never the right choice. Use them only when an obscure tool refuses to accept anything else.
The minimal-encoding sweet spot
The smallest correct encoding for modern browsers escapes only the characters that genuinely conflict with the surrounding CSS or HTML syntax. For CSS background-image with single-quoted URLs, that’s a remarkably short list:
#→%23— required, because#begins a URL fragment%→%25— required, because it’s the escape character"→%22— required if yoururl(...)uses double quotes; not required if it uses single quotes'→%27— required if your URL uses single quotes
That’s it. Modern browsers happily accept literal <, >, spaces, and most other characters inside data URIs. The famous Stoyan Stefanov post “The Truth(tm) about encoding SVG in data URIs” is where the web first realised this; a decade later, it’s the consensus.
/* Bad: base64 (424 bytes for our test icon) */
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0i...');
/* Bad: full URL encoding (412 bytes) */
background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D...');
/* Good: minimal encoding (322 bytes) */
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M..." fill="%23111827"/></svg>');The only escapes in the minimal version are the # in the colour code (because it would otherwise terminate the URL). Notice how the SVG markup remains human-readable and grep-able.
The CSS syntax in detail
Putting an SVG into a CSS background-image works identically to putting any other URL there:
.icon-arrow {
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7" stroke="%23111" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>');
background-repeat: no-repeat;
background-position: center;
background-size: 16px;
width: 24px;
height: 24px;
}Two tactical notes:
- Quote the URL. Some CSS parsers and many SCSS compilers will choke on unquoted data URIs because the SVG contains characters they treat as significant. Always wrap in single (or double) quotes inside
url(). - Pick a quote style and stick to it. If your
url()uses single quotes, use double quotes inside the SVG markup, and vice versa. Mixing causes the URL to terminate prematurely.
When to inline as data URI vs. external file
Data URIs are not a free lunch. They’re great in some situations and counterproductive in others:
Use a data URI when:
- The SVG is small (under ~2 KB after encoding) and used in CSS (background-image, list-style-image, etc.) where you cannot inline it as
<svg>markup. - The asset is unique to a single CSS file (not reused across pages), so caching is irrelevant.
- Saving the HTTP request matters for above-the-fold rendering (think: hero background pattern, button arrow icon).
- You’re building a self-contained component or a Stripe-style embeddable script that needs to ship without external assets.
Use an external SVG file when:
- The SVG is large (over ~5 KB). The break-even point depends on HTTP/2 vs HTTP/1.1, but as a rule of thumb anything bigger than a small icon should be its own file.
- The asset is reused across many pages and you want to share the cache.
- You need to manipulate the SVG with JavaScript (DOM access only works for inline SVG, not for SVGs loaded as background images).
- You need accessibility (data-URI background images are invisible to screen readers — they’re considered decorative). For informative icons, use inline
<svg>with proper aria attributes.
For the in-between case — an icon that’s reused throughout an app and needs CSS theming — consider an SVG sprite loaded once and referenced via <use>.
The CSS-variable theming trick
One of the most powerful patterns: data URIs that read CSS variables for their colour. You can’t put a var(--accent) inside the SVG markup directly — the data URI is parsed as a string, not as nested CSS — but you can use the data URI as a mask:
.icon-arrow {
width: 24px;
height: 24px;
background-color: var(--icon-color);
-webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7" stroke="black" stroke-width="2" fill="none"/></svg>');
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7" stroke="black" stroke-width="2" fill="none"/></svg>');
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
}The SVG defines the shape; the CSS background-color defines the colour. Universal browser support since 2023. This single trick is why data URIs are still relevant in 2026 even with inline SVG and SVG sprites available.
Tools that produce minimal-encoded URIs
Almost all online encoders default to base64. The good ones with a minimal-encoding option:
- yoksel.github.io/url-encoder — paste an SVG, get the minimal-encoded data URI. The de facto standard tool.
- mini-svg-data-uri — a tiny npm package for build-time encoding. Wraps the same logic in a programmatic API. Used by webpack/postcss/Vite plugins.
- postcss-inline-svg — PostCSS plugin that lets you reference SVG files from CSS and have them inlined as data URIs at build time, with theming via SCSS-style variable substitution before encoding.
For one-off conversions, the Yoksel tool is the fastest path. For a design system shipping dozens of icons via CSS, automate it through PostCSS or the bundler so re-exports are painless.
Real numbers from a real icon
To make the trade-offs concrete, here’s the same 24×24 arrow icon in five flavours:
- External SVG file, optimised: 318 bytes, 1 HTTP request, fully cached
- Inline
<svg>in HTML: 318 bytes, no extra request, no caching - Minimal URL-encoded data URI: 322 bytes (~1.3% overhead)
- Fully URL-encoded data URI: 412 bytes (~30% overhead)
- Base64 data URI: 424 bytes (~33% overhead)
The cost of choosing the wrong encoding for one tiny icon is small (102 bytes). The cost across an icon-heavy CSS file with fifty icons is multi-kilobyte. Across a design system used by a hundred sites, it’s a measurable amount of bandwidth.
The summary
Data URIs for SVG: yes, in the right situations (small, in-CSS, used locally, no JS or accessibility needs). The right encoding: minimal URL-encoding via Yoksel’s tool or mini-svg-data-uri, never base64. The right pattern for theming: CSS mask-image with a background-color, not background-image with a hard-coded fill.
And as always, before encoding anything: run the SVG through SVGO first. Encoding bloat is baked into whatever you encode.
Related on svg.dog: Inline vs external SVG for the bigger embed-strategy decision and the canonical reference Probably Don’t Base64 SVG on CSS-Tricks.
Optimize SVGs instantly — no setup needed
500 free optimizations per month. No credit card required.
Get a free API key →