Logo
Back to Blog
Web Development August 8, 2026 9 min read

Base64 Images in CSS and HTML: The Complete Data URI Guide

O

OmniWebKit Team

Web Performance Engineer

Share:
Article Cover Image

You inline an icon to save a request. Two months later your stylesheet is 400 KB, first paint has slipped, and nobody can see why. Base64 images in CSS are a real technique with a real cost, and most guides only tell you half of it.

Here is the whole picture: how to do it, what breaks, and when to stop.

How a Base64 Image in CSS Actually Works

A Data URI puts the file inside the URL rather than pointing at one. The browser reads the bytes straight from your stylesheet:

.icon-search {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANS...");
  background-size: 16px 16px;
  background-repeat: no-repeat;
}

Four parts do the work. data: names the scheme, image/png is the MIME type, ;base64 says the payload is encoded, and everything after the comma is the file.

Always wrap the value in quotes. Unquoted url() has its own escaping rules and they will surprise you.

Generate the string with our image to Base64 converter — it outputs a finished CSS declaration, so there is nothing to assemble by hand.

Inline Images in HTML with an img src

The markup version is simpler, because HTML has none of the CSS escaping problems:

<img src="data:image/png;base64,iVBORw0KGgoAAAANS..." alt="Search">

Write real alt text. A Data URI is opaque to every assistive technology, so the alt attribute is the only description a screen reader will ever get.

One practical difference from CSS: an inline image in HTML does not block rendering the way a stylesheet does. The browser paints around it. That makes HTML the safer place to inline when you have the choice.

Data URI in a CSS Background: The Escaping Traps

Base64 output is alphanumeric, so it survives anything. SVG is where people get hurt, because percent-encoded SVG is smaller and everyone wants to use it.

CharacterEscape asWhat breaks if you forget
#%23Every hex colour truncates the URL
%%25Corrupts all other escapes
< >%3C %3EEvery tag in the file
{ }%7B %7DEmbedded style blocks
"Use ' insteadCloses the url() early

The hash is the one that catches everyone. An SVG with a single fill="#333" in it will silently render nothing.

Our SVG to Base64 encoder produces both encodings side by side and escapes all of this for you.

The Caching Cost Nobody Mentions

This is the argument that should decide it, and it is the one usually left out.

A normal image file is cached on its own. First visit downloads it; every later visit and every other page reuses it from disk. That is one download, ever.

An inlined image is part of the stylesheet. Change one CSS rule and the whole file — every embedded image with it — is downloaded again. Split the image across two stylesheets and it downloads twice.

In our experience this is where inlining quietly loses. The saved request looks good on a first-load waterfall and costs you on every visit after that.

The 33% size penalty compounds it. A 2 KB icon costs about 700 extra bytes, which is nothing. A 200 KB illustration costs 66 KB, which is not.

The CSP Rule That Blocks Everything

One that catches teams the first time they harden a site:

Content-Security-Policy: img-src 'self'

That policy blocks every Data URI on the page. The browser console shows a CSP violation, not a broken image, so the first instinct is to blame the encoding.

The fix is to allow the scheme explicitly:

Content-Security-Policy: img-src 'self' data:

Do it deliberately, though. Allowing data: in script-src or object-src is a genuine security hole, and copying the pattern across directives is how that happens.

When Not to Inline

Our rule of thumb: under 2 KB inline freely, 2 to 10 KB think about it, above 10 KB do not.

Skip inlining entirely when any of these apply:

  • The image appears on many pages. A cached file wins every time.
  • It is your LCP element. Hero images need to start downloading early, not wait for CSS to parse.
  • You use responsive images. Srcset and Data URIs work against each other.
  • It is going in an email. Outlook strips Data URIs.
  • The asset changes often. Every update invalidates the whole stylesheet.

And be honest about HTTP/2. The original case for inlining was request overhead, and multiplexing removed most of it. The technique survived the reason for it.

Where Inlining Still Wins

Four cases where we still reach for it:

  • Tiny critical icons needed for the first paint.
  • Single-file HTML — reports and exports that must work with no network.
  • Generated content such as QR codes and charts that exist only for one page.
  • Offline-first apps where every asset ships in the bundle.

Notice the pattern. Inlining is for images that are small, stable, and needed immediately.

Wrapping Up

Using a Base64 image in CSS is easy. Knowing when not to is the skill.

Inline the small, critical, unchanging things. Host everything else and let the browser cache do its job. When you do inline an SVG, escape the hash characters or watch it disappear.

For the numbers behind the trade-off, see when Base64 images are worth it. To generate the strings, use our PNG to Base64 encoder, and to read one back, the Base64 image decoder.

Frequently Asked Questions

Does inlining still beat HTTP/2 multiplexing?

+
Rarely now. HTTP/2 removed most of the per-request overhead that made inlining worthwhile, and the browser can cache separate files. Inlining only wins when the image is tiny and needed for the very first paint.

Why does my Data URI get blocked by Content-Security-Policy?

+
Because a default img-src or style-src directive does not allow the data: scheme. You have to name it explicitly, as in img-src self data:. Teams hit this the first time they ship a CSP and assume the image itself is broken.

Does a Base64 background hurt Largest Contentful Paint?

+
It can go either way. Inlining removes a network round trip, which helps, but the payload sits inside a render-blocking stylesheet, which hurts. For anything above a few kilobytes the blocking cost usually wins.

Will Lighthouse flag my inline images?

+
Not directly, and that is part of the problem. Lighthouse audits image encoding and sizing, but a Base64 blob inside CSS is invisible to those checks. Your stylesheet just quietly grows and nothing warns you.

Can I use a Data URI for a favicon?

+
Yes, with a link tag pointing at the Data URL, and it saves a request on first load. Some feed readers and older browsers ignore it though, so keep a real favicon file as a fallback.

Why does my SVG Data URI break when it contains a colour?

+
The hash character. Inside CSS a raw # starts a fragment identifier and cuts your URL in half, so any fill="#333" destroys the image. Escape it as %23.

Do CDNs compress Data URIs the way they compress normal files?

+
Gzip and Brotli do run over your stylesheet, so the Base64 padding compresses somewhat. But PNG and JPEG data is already compressed, so there is little left to squeeze. Expect a real cost of a few percent, not zero.

Should I inline images in email templates?

+
It is tempting, since many clients block remote images by default. But desktop Outlook strips Data URIs entirely and shows a broken placeholder. We recommend hosted images with a plain-text fallback instead.

Can I put a Data URI in srcset for responsive images?

+
Technically yes, and it defeats the entire purpose. Srcset exists so the browser downloads only the size it needs, whereas inlining forces every variant into the HTML for every visitor.

Does lazy loading work on an inline image?

+
No, and there is nothing to lazy-load. The bytes already arrived with the document, so the loading attribute has no effect. Inlining and lazy loading are opposite strategies.

Tags

#CSS#HTML#Data URI#Base64#Performance