web3icons

Using SVG in CSS: data URIs, masks and backgrounds

How to encode SVG for CSS data URIs, when to use background-image versus mask-image, and when a separate SVG file is the better choice for caching.

5 min read

CSS can display an SVG without any markup in your HTML: as a background, as a mask, or inside pseudo-elements like ::before. The SVG can live in a separate file, or be embedded straight into the stylesheet as a data URI. Each choice changes how the icon is colored, how it is cached, and how easy the CSS is to maintain.

Three ways to point CSS at an SVG

/* 1. A file */
.a { background-image: url("/icons/check.svg"); }

/* 2. A URL-encoded data URI */
.b { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'...%3C/svg%3E"); }

/* 3. A base64 data URI */
.c { background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLi4u"); }

The first is an ordinary request. The other two embed the file in the stylesheet, so there is no extra request at all.

URL-encoding vs base64

Because SVG is text, you do not need base64. A URL-encoded SVG is usually the better choice:

URL-encoded Base64
Size before compression Close to the original About a third larger
After gzip or Brotli Compresses well, like the original text Compresses noticeably worse
Readable in the stylesheet Mostly No
Escaping needed A handful of characters None, but you need an encoder

Base64 turns every 3 bytes into 4 characters, and it scrambles the repeated patterns that compression relies on. Its only advantage is that you never have to think about which characters to escape. Use it if your tooling produces it automatically and the icons are tiny; otherwise prefer URL-encoding.

What to escape

Inside a url("data:image/svg+xml,...") value, a few characters cause trouble:

  • # must become %23. In a URL, # starts a fragment, so fill="#a1a1aa" silently cuts the SVG off at the #. This is the most common reason a data URI icon renders as nothing.
  • % must become %25, since it starts an escape sequence.
  • < and > should become %3C and %3E. Some browsers accept them raw, but encoding them is safer.
  • Quotes: wrap the url() in double quotes and use single quotes inside the SVG, or the other way round.
  • Line breaks: remove them, or encode them.

Here is a complete, working example, a check mark in neutral gray:

.done::before {
  content: "";
  display: inline-block;
  width: 1em;
  height: 1em;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10 15l-3-3-1.5 1.5L10 18l8-8-1.5-1.5z' fill='%23a1a1aa'/%3E%3C/svg%3E") center / contain no-repeat;
}

The xmlns attribute is required. A data URI is a standalone SVG document, and without the namespace it will not render.

Encoding by hand is error-prone, so let code do it. encodeURIComponent escapes everything that needs escaping (and a bit more, which costs a few bytes but is always correct):

function svgToCssUrl(svg) {
  const compact = svg.replace(/\s*\n\s*/g, ' ').trim()
  return `url("data:image/svg+xml,${encodeURIComponent(compact)}")`
}

Run this in your build step, or use whatever your bundler offers. Many bundlers inline small assets as data URIs automatically below a size threshold; check your bundler's settings to see which encoding it uses and where the threshold sits.

background-image vs mask-image

This is the more important decision, because it determines who controls the color.

background-image shows the SVG as it is. The colors are whatever is written in the file. A multi-color brand logo looks exactly like the original. You cannot change the colors from CSS, and currentColor inside the data URI does not follow your page: the SVG is a separate image document, so currentColor there resolves to that document's default, usually black.

mask-image uses the SVG as a stencil. The shape's opaque areas let the element's own background show through, and transparent areas hide it. The color comes from background-color, so it can follow your theme, your hover state or the text color:

.icon-copy {
  width: 20px;
  height: 20px;
  background-color: currentColor;
  -webkit-mask: url("/icons/copy.svg") center / contain no-repeat;
  mask: url("/icons/copy.svg") center / contain no-repeat;
}
.button:hover .icon-copy { color: #fafafa; }

Masks only make sense for single-color icons. Applied to a branded logo, a mask turns every colored part into one flat silhouette. Older versions of Chromium-based browsers and Safari only understand the -webkit- prefixed properties, so write both. Recoloring a single-color SVG with CSS mask goes deeper into sizing, positioning and support.

background-image mask-image + background-color
Colors come from The SVG file Your CSS
Multi-color logos Shown correctly Flattened to one color
Follows color and themes No Yes, with background-color: currentColor
Typical use Brand logos, decorative patterns Interface icons, mono logos

If you need an icon to follow text color and you can put markup in the HTML, inline SVG with fill="currentColor" is the other option; currentColor in SVG explains it.

Accessibility

CSS images are invisible to assistive technology. That is correct for decoration: a check mark before a list item that already says "Completed", or a chevron in a button that has a text label. If the icon is the only thing conveying meaning, such as an icon-only button, the element needs a name of its own, for example aria-label="Copy address" on the button.

Data URI or separate file?

Embedding is convenient, but it moves weight into your stylesheet, and stylesheets block rendering. Use this as a guide:

Situation Choose
A few small icons (under 1 to 2 KB each) used on most pages Data URI
Icons that are part of a component's styling (checkbox tick, select chevron) Data URI
Large or detailed SVGs Separate file
Icons only used on one page or in one state Separate file
The same icon used in many different rules Separate file, or one rule shared via a class
Icons that change more often than the rest of the CSS Separate file

Separate files have three advantages that grow with scale. They are cached independently, so changing one icon does not invalidate your whole stylesheet. Browsers generally fetch them only when an element on the page actually uses the rule. And the same file can be shared between CSS, <img> tags and other stylesheets without being duplicated. Give them long cache lifetimes and content-hashed names, as described in caching SVG files with HTTP headers.

Data URIs win when an icon is tiny, needed for the first render, and used everywhere, because it then arrives with the CSS and never flashes in late.

More in SVG

All guides