Icons in Pure CSS
In my previous post about Reimagine Atomic CSS, I introduced a preset of UnoCSS that provides the ability to use any icons on-demand in purely CSS. Today in this post, I’d like to share with you how we made it possible.
My Icon Explorations #
If you are interested in how I get here, there is an index of my previous post about the stories of my icon explorations and experiments.
- Aug. 2020 — Journey with Icons
- Sep. 2021 — Journey with Icons Continues
- Oct. 2021 — Reimagine Atomic CSS (The CSS Icons Preset)
- Nov. 2021 — Icons in Pure CSS — you are here!
Prior Arts #
I know there is a Pure CSS icon solution called css.gg , which is a great idea to use pseudo-elements ( ::before , ::after ) to construct the icons. However, that could require some expert knowledge of how CSS works, but I imagine that approach could be hard to create more complex icons. Instead of the limited choices in a specific set, I am seeking a more general solution that could apply to any icons.
The Idea #
The idea come from this feature request created by @husayt to unplugin-icons and the initial implementation in this pull request by @userquin. The idea here is quite straightforward — to generate CSS with the icons in DataURI as the background image.
.my-icon background: url(data. ) no-repeat center; background-color: transparent; background-size: 16px 16px; height: 16px; width: 16px; display: inline-block; >
.my-icon background: url(data. ) no-repeat center; background-color: transparent; background-size: 16px 16px; height: 16px; width: 16px; display: inline-block; >
With that, we could use any images inlined in CSS with a single class.
It’s indeed an interesting idea. However, this is more like an image instead of an icon. To me, an icon has to be scalable and colorable (if it’s monochrome).
Make it Work #
DataURI #
Thanks again to Iconify, which unified 100+ icon sets with 10,000+ icons into the consistent JSON format. It allows us to get the SVG of any icon set by simply providing the collection and icon ids. The usage is like this:
import iconToSVG, getIconData > from '@iconify/utils' const svg = iconToSVG(getIconData('mdi', 'alarm')) // (this is not the exact API, simplified here for demo)
import iconToSVG, getIconData > from '@iconify/utils' const svg = iconToSVG(getIconData('mdi', 'alarm')) // (this is not the exact API, simplified here for demo)
Once we got the SVG string, we could convert the it to DataURI:
const dataUri = `data:image/svg+xml;base64,$Buffer.from(svg).toString('base64')>`
const dataUri = `data:image/svg+xml;base64,$Buffer.from(svg).toString('base64')>`
Talking about DataURI, it’s almost the default choice to use Base64 until I read Probably Don’t Base64 SVG by Chris Coyier. Base64 is needed to encode binary data like images to be used in plain text files like CSS, while for SVG, since it’s already in text format, the extra encoding to Base64 actually makes the file size larger.
Combine the technique mentioned in Optimizing SVGs in data URIs by Taylor Hunt to improve the output size, further, here is the solution we end up with.
// https://bl.ocks.org/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 function encodeSvg(svg: string) return svg.replace('', (~svg.indexOf('xmlns') ? '' : '')) .replace(/"/g, '\'') .replace(/%/g, '%25') .replace(/#/g, '%23') .replace(//g, '%7B') .replace(/>/g, '%7D') .replace(//g, '%3C') .replace(/>/g, '%3E') > const dataUri = `data:image/svg+xml;utf8,$encodeSvg(svg)>`
// https://bl.ocks.org/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 function encodeSvg(svg: string) return svg.replace('', (~svg.indexOf('xmlns') ? '' : '')) .replace(/"/g, '\'') .replace(/%/g, '%25') .replace(/#/g, '%23') .replace(//g, '%7B') .replace(/>/g, '%7D') .replace(//g, '%3C') .replace(/>/g, '%3E') > const dataUri = `data:image/svg+xml;utf8,$encodeSvg(svg)>`
Scalable #
The first step of making the «image» more like an icon, we need to make it scalable to the context.
Luckily we have the first-class support scaling support — the em unit.
.my-icon background: url(data. ) no-repeat center; background-color: transparent; background-size: 100% 100%; height: 1em; width: 1em; >
.my-icon background: url(data. ) no-repeat center; background-color: transparent; background-size: 100% 100%; height: 1em; width: 1em; >
By changing the height and width to 1em , and the background-size to 100% , we made the image scales based on the parent’s font size.
Colorable #
In inlined SVG, we could use fill=»currentColor» to make the color of the SVG matches with the current text color. However, when we use it as a background image, it becomes a flat image. The dynamic parts of the SVG are lost, so is the currentColor magic (it’s just like you can’t override the color of a PNG).
If you do a quick search, you will find that most people are telling you that you can’t. Some might offer you the option to assign the colors in the SVG before converting to DataURI, which could solve the specific problem that you want the icon to have color, but not the root cause that the color is not reactive to the context.
Then you might come up with the idea of using CSS filters, like Una Kravets mentioned in Solved with CSS! Colorizing SVG Backgrounds. That sounds valid, but only that you need to calculate the matrix of how to transform the color to the desired ones. Probably feasible by introducing some runtime JavaScript for that? Maybe, if so, we lost the whole point of trying icons in pure CSS.
This sounds like a dead-end to me. Until I accidentally found the article Coloring SVGs in CSS Background Images by Noah Blon. In the article, Noah mentioned a brilliant idea of using CSS masks — a property that I have never heard of before.
.my-icon background-color: red; mask-image: url(icon.svg); >
.my-icon background-color: red; mask-image: url(icon.svg); >
Instead of using the icon as a background image and figuring out a way to color it, we could actually use the icon as a mask to clip the filled background color. Furthermore, we could now use the currentColor magic to have the icon matching with the parent text color!
.my-icon background-color: currentColor; mask-image: url(icon.svg); >
.my-icon background-color: currentColor; mask-image: url(icon.svg); >
Using CSS to Create SVG Icons
Usage of SVG for web icons continues to grow given more widespread browser support and the advantages over other approaches: font libraries require an additional external resource full of icons you’ll never use while raster images don’t scale to different resolutions and have larger file sizes than SVG. There are several approaches to implementing an SVG icon system, each with its own benefits and drawbacks.
This post demonstrates a pure CSS/Sass approach that works in IE10 and greater and other major browsers and allows for reusable SVG icons that can be styled for color and size:
Getting started
For the sake of brevity the examples use a simple circle element but the approach works with any sort of SVG content; most icons will likely be path elements. See the end of this post for code examples to create the icons displayed above.
The goal is to end up with compiled CSS that looks something like the following:
background-image: url ( ‘data:image/svg+xml;charset=utf-8,%3Csvg viewBox=%220 0 10 10%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Ccircle fill=%22%23fd9827%22 cx=%225%22 cy=%225%22 r=%225%22 /%3E%3C/svg%3E’ );
Note that the “ , # , < , and >characters are encoded as required by Firefox and IE (don’t worry, we do the encoding with functions). Applying the icon-circle class to an HTML element (e.g., ) yields a 32×32 orange circle:
This tutorial focuses on how to generate the background-image property value using Sass functions. The raw Sass file that outputs the above CSS will be as follows:
Defining utility functions
From the above we know that we need to define a function get-icon-circle that takes a fill color as its single argument and returns a background-image property value. Typically I’ll create a file _icons.scss that includes all icon-related functions though the following can obviously be included anywhere in your project to suit your needs:
@return str-slice ( $string , 1 , $index — 1 ) + $replace + str-replace ( str-slice ( $string , $index + str-length ( $search )), $search , $replace );
- Line 2 $encodings is a map of characters to their encoded values.
- Line 10 The str-replace function replaces all occurrences of $search in $string with $replace . This function was developed by Hugo Giraudel; the original is available here.
- Line 19 The get-icon function returns a background-image property value. It accepts two arguments: $content , any SVG content string and $viewBox , the value for the SVG’s viewBox attribute. The function wraps the provided content in tags, encodes the result and packages it up into a valid URL.
Defining icon functions
With the utility functions defined we can now define the get-icon-circle function that we used above:
The get-icon-circle function takes a $fill color that’s inserted into the icon’s SVG definition and returns a URL for use as a background-image property value. From here we can follow the same template to define functions to generate other icons. By way of example we’ll define the functions used to generate the @, cog, and anchor icons shown at the beginning of the post:
$content : ‘
$content : ‘
$content : ‘
Using the icon functions
Again assuming that we’ve defined our icon functions in a file _icons.scss we can use them in other .scss files as follows: