web3icons

Recoloring a single-color SVG with CSS mask

Use mask-image and background-color to paint a mono SVG icon in any color straight from a URL, with prefixes, sizing, browser support and accessibility.

5 min read

An <img> tag can't change an SVG's color, and inlining every icon to reach its fill bloats your HTML. CSS masks sit in between. You point mask-image at a single-color SVG file, set background-color, and the icon appears in that color. One file serves every color, theme and hover state.

This works only for single-color artwork, the mono style of a logo. Below is the basic pattern, a reusable version, the prefix situation, and the edge cases that catch people out.

How the mask works

A mask decides which parts of an element are visible. With an image as the mask, the browser uses the image's alpha channel by default: fully opaque pixels show the element, transparent pixels hide it, and semi-transparent pixels, such as anti-aliased edges, show it partly.

So if an element has a solid background color and a mono icon as its mask, you see the background color in the shape of the icon:

.icon-eth {
  display: inline-block;
  width: 24px;
  height: 24px;
  background-color: #e6e8ec;
  mask-image: url("/icons/mono/eth.svg");
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
}

The fill color inside the SVG file doesn't matter. White, black or red, only the opacity of each part counts. That is also why masks don't suit multi-color logos: every opaque area takes the same color, so a branded logo collapses into a flat silhouette and loses whatever detail its colors were carrying.

This is how the mono previews in the Web3 Icons library are drawn: one file per icon, filled with whichever color you pick.

A reusable icon class

Writing a class per icon gets repetitive. A custom property keeps the file URL separate from the styling:

.icon {
  --size: 1.25em;
  display: inline-block;
  flex: none;
  width: var(--size);
  height: var(--size);
  background-color: currentColor;
  -webkit-mask: var(--icon) center / contain no-repeat;
  mask: var(--icon) center / contain no-repeat;
}
<span class="icon" style="--icon: url('/icons/mono/sol.svg')" role="img" aria-label="Solana"></span>

Three details are doing the work here.

display: inline-block and a size. An empty <span> is inline and zero pixels wide. Without explicit dimensions, the mask has nothing to show.

em sizing. At 1.25em, the icon grows and shrinks with the surrounding font size, so a 14 px label gets a 17.5 px icon and a 20 px heading a 25 px one. Use a fixed pixel size instead when the icon must align to a grid.

background-color: currentColor. The icon takes the element's text color, so hover styles, disabled states and themes work the same way they do for text:

.nav-link { color: #8b9099; }
.nav-link:hover,
.nav-link[aria-current="page"] { color: #ffffff; }

An icon inside .nav-link follows along with no extra rules. The currentColor guide covers the same idea for inline SVG.

Keep mask-size: contain (or 100% 100% if you are certain the file is square). Without it, the mask renders at the SVG's own width and height, which may not match the element. mask-position: center and no-repeat keep a slightly non-square file from being cropped or tiled.

Prefixes and browser support

Chromium-based browsers and Safari supported masks for years only with the -webkit- prefix. Unprefixed support across the major engines arrived only around 2023, while Firefox had it much earlier. Current versions of Chrome, Edge, Firefox and Safari all handle the unprefixed form.

If your audience includes older browsers, and wallet users on older phones are a real audience, write both, prefixed first. The -webkit-mask shorthand accepts the same image position / size repeat syntax used above. For exact version numbers, check the compatibility table on MDN's mask-image page; it changes faster than any article.

Where masks aren't supported at all, the element shows as a solid colored square. If that matters, move the background-color declaration into a feature query, so unsupported browsers show nothing instead of a block:

@supports (mask-image: none) or (-webkit-mask-image: none) {
  .icon { background-color: currentColor; }
}

Data URIs and cross-origin files

You can embed the icon directly in the stylesheet:

.icon-check {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m9 16.2-4.2-4.2-1.4 1.4L9 19 21 7l-1.4-1.4z'/%3E%3C/svg%3E");
}

Encode <, > and especially # (as %23), which would otherwise end the URL early. Data URIs save a request but are repeated wherever the rule is, and they make the stylesheet harder to read. They suit a handful of small UI icons, not a catalog of 500 logos. The SVG in CSS guide compares the options.

If the SVG lives on another origin, such as a CDN, the browser may refuse to use it as a mask unless that server sends CORS headers. If an icon works locally and disappears in production, check the network panel for a blocked request. Serving icons from your own origin avoids the issue.

Accessibility

A masked element has no content. To assistive technology it is an empty <span>, so the icon needs either a name or a clear signal that it is decorative.

  • Meaningful on its own (a network badge with no visible label, an icon-only button's graphic): give the element role="img" and an aria-label, as in the Solana example above. If it sits in a button, put the label on the button instead; the icon-only buttons guide shows the pattern.
  • Next to text that says the same thing: add aria-hidden="true", or simply leave the empty span unlabeled.

Two display modes deserve a test.

Windows forced colors mode replaces author background colors with system colors, which can turn a masked icon into the page's background color, making it invisible. Opt the icon out and give it a system color:

@media (forced-colors: active) {
  .icon {
    forced-color-adjust: none;
    background-color: CanvasText;
  }
}

Chromium's developer tools can emulate forced-colors: active in the Rendering panel, so you can check this without a Windows machine.

Printing. Browsers usually skip background colors when printing unless the user enables background graphics, so masked icons often vanish from printed pages. That is rarely a problem for interface icons, but don't rely on a masked icon to carry information on a page people print, such as a transaction receipt.

When to use something else

A mask is the right tool for single-color icons that need several colors and should stay as cached files. Reach for inline SVG when you need two colors, animation of individual parts, or a <title> inside the graphic. Use a plain <img> for branded logos, which keep their own colors and need no recoloring at all. And if a logo has no mono version, the mask can't make one; the mono versions guide covers how that artwork is drawn.

More in Using crypto logos

All guides