Why pure CSS icons are the best

Preview sketch of some icons

If you needed an icon for your website, how would you go about it? Find a nice png that suits your vision? Perhaps an svg, so you have more control over its size? Would you use an icon font? Or would you, like me, try to create the icon with pure CSS? In this post, I will explain why the last option is so awesome and why you should do that.

So... Why?

So let me reiterate the options I described above; png, svg, font, and CSS. I think we can agree that the first option is pretty bad. You can't scale it, it's larger than the svg, so on all fronts, the svg option just wins. Do fonts win over svg's? Well, that's a bit situational - if you want to use a lot of different icons, or like your HTML compact, then using fonts may win. But, if you're only using one or two icons, perhaps those svg's may be the better choice, as loading a whole font for two little icons is a bit overkill. However, now comes the (in my opinion) king of them all; pure CSS. It is similar to svg in that it is scalable, but it doesn't clutter your HTML at all. Size wise, they are similar in size or smaller than the svg. And, the best part that none of the answers have, is: animations. Pure CSS icons are super easy to animate and you can create smooth transitions between icons. It just makes your page feel sleak, detailed, and it shows your user that you cared.

What's the catch?

Alright, alright, I admit it. Pure CSS icons have their limitations. But, I still wholeheartedly believe in them. In fact, they're super fun to make just because of their limitations. Obviously, when we write our HTML, we want just a single element, to avoid the clutter. We don't want to be making images with HTML, we just want the HTML to have an icon, and the CSS to do the hard work (after all, CSS is supposed to style, not HTML). That means we only have one element to work with! But, it limits your options less than you think it does. Because really, we have three elements; the element itself, and the ::before and ::after pseudo-elements. Additionally, we can use transform, box shadow, border radius, clip path, and a bunch of other powerful CSS properties. So really, we have quite a bit of freedom.

An example - the menu icon

So let's try to make the menu icon that transitions into a close icon (an X) when toggled. So the first question we have to ask ourselves is: how many psuedo-elements do we need? Well, for the three parallel lines, we don't need any psuedoelements. We simply make one line, and use a box shadow for the other two. For the X though, that won't work, since you can't transform box shadows, and the lines are rotated by 90 degrees relatively. So for the close icon, we need at least one pseudo-element. To create a smooth transition from and to the menu icon, instead of using two box shadows, one of the lines will be the element itself, one will be a pseudo-element, and the last will be a box shadow. So, here's what that looks like:

<i class="menu-icon"></i>
.menu-icon { font-size: 60px; color: black; display: block; position: relative; width: 1em; height: .1em; background-color: currentColor; border-radius: .05em; box-shadow: 0 .3em currentColor; transition: .3s; } .menu-icon::after { content: ""; position: absolute; left: 0; top: -.3em; width: 100%; height: 100%; background-color: inherit; border-radius: inherit; transition: .3s; } .menu-icon[data-toggled] { transform: rotate(45deg); box-shadow: 0 0 currentColor; } .menu-icon[data-toggled]::after { top: 0; transform: rotate(90deg); } /* just to center the icon */ body { display: flex; justify-content: center; align-items: center; cursor: pointer; margin: 0; height: 100vh; }
const body = document.querySelector('body'); const icon = document.querySelector('i'); body.addEventListener('click', () => { if(icon.hasAttribute('data-toggled')){ icon.removeAttribute('data-toggled'); } else { icon.setAttribute('data-toggled', ''); } });

You have to admit; that will beat any static image. Even if it's just a small effect, it's a really nice detail. And, the beauty of it is that we can adjust it easily; it scales with the font size, and we can use the color property to set the color. Additionally, we can, instead of using the element itself and one pseudo-element, just use two pseudo-elements, so that the actual element itself can be the button to click on. That way, all the HTML we need for a menu button would be

<button id="menu-button"></button>

instead of the slightly-uglier

<button id="menu-button">
	<i class="menu-icon"></i>
</button>

You might want to add an aria-label attribute to the button for accessibility, but the same goes for images and icon fonts.

More!

So, that above example was a bit simple. You may think, well, if that's the caliber of things I can do, I'll stick with my font icons. No! Wait. I was just explaining the concepts. I'll show you some more complex ones below. These are all built from only two psuedo-elements, so imagine what you can do if you're using the element itself as well!

<button class="nightmode icon"></button> <button class="visibility icon"></button> <button class="add icon"></button> <button class="secure icon"></button> <button class="switch icon"></button> <button class="check icon"></button> <button class="sound icon"></button> <button class="timer icon"></button> <button class="play icon"></button>
.icon { position: relative; font-size: 40px; width: 2em; height: 2em; background-color: transparent; border-width: 0; cursor: pointer; transition: .3s; padding: 0; margin: 0; } .icon:hover { background-color: rgba(0, 0, 0, .1); } .icon::after, .icon::before { content: ""; position: absolute; left: 50%; top: 50%; transition: .3s; } .icon.nightmode::after { width: .8em; height: .8em; border-radius: 50%; box-shadow: .2em -.1em 0 .1em inset currentColor, 0 0 0 -.3em currentColor, 0 0 0 -.3em currentColor, 0 0 0 -.3em currentColor, 0 0 0 -.3em currentColor, 0 0 0 -.3em currentColor, 0 0 0 -.3em currentColor, 0 0 0 -.3em currentColor, 0 0 0 -.3em currentColor; margin: -.4em 0 0 -.4em; } .icon.nightmode[data-toggled]::after { box-shadow: .4em -.1em 0 .4em inset currentColor, -.6em 0 0 -.3em currentColor, .6em 0 0 -.3em currentColor, 0 -.6em 0 -.3em currentColor, 0 .6em 0 -.3em currentColor, -.42em -.42em 0 -.3em currentColor, -.42em .42em 0 -.3em currentColor, .42em -.42em 0 -.3em currentColor, .42em .42em 0 -.3em currentColor; } .icon.visibility::after { width: .8em; height: .8em; border-radius: .7em 0; margin: -.4em 0 0 -.4em; box-shadow: 0 0 0 .15em inset currentColor; transform: rotate(45deg); } .icon.visibility::before { width: .24em; height: .24em; margin: -.12em 0 0 -.12em; border-radius: .12em; background-color: currentColor; transform: rotate(45deg); } .icon.visibility[data-toggled]::before { width: .15em; height: 1em; margin: -.5em 0 0 -.075em; } .icon.add::after { width: .15em; height: 1em; margin: -.5em 0 0 -.075em; border-radius: .075em; background-color: currentColor; } .icon.add::before { width: 1em; height: .15em; margin: -.075em 0 0 -.5em; border-radius: .075em; background-color: currentColor; } .icon.add[data-toggled]::after { height: .15em; margin-top: -.075em; transform: rotate(360deg); } .icon.add[data-toggled]::before { transform: rotate(360deg); } .icon.secure { --keyhole-color: lightgrey; } .icon.secure::after { width: .3em; height: .3em; border-radius: 50%; margin: -.6em 0 0 -.3em; border: .15em solid currentColor; border-bottom-color: transparent; border-right-color: transparent; box-shadow: .33em .33em 0 -.2em var(--keyhole-color); transform: rotate(45deg); } .icon.secure::before { width: .15em; height: .15em; background-color: currentColor; margin: -.3em 0 0 -.3em; box-shadow: .45em 0 currentColor, .45em .15em currentColor, .225em .50em 0 -.02em var(--keyhole-color), .225em .55em 0 -.02em var(--keyhole-color), .225em .45em 0 .3em currentColor; } .icon.secure[data-toggled]::after { margin: -.65em 0 0 -.3em; box-shadow: .44em .44em 0 -.2em var(--keyhole-color); } .icon.secure[data-toggled]::before { margin: -.35em 0 0 -.3em; box-shadow: .45em 0 currentColor, .45em .15em currentColor, .225em .65em 0 -.02em var(--keyhole-color), .225em .7em 0 -.02em var(--keyhole-color), .225em .6em 0 .3em currentColor; } .icon.switch::after { width: .3em; height: .3em; margin: -.15em 0 0 -.45em; border-radius: .15em; background-color: currentColor; } .icon.switch::before { width: 1em; height: .4em; border-radius: .2em; margin: -.2em 0 0 -.5em; box-shadow: 0 0 0 .15em currentColor, 0 0 0 0 inset currentColor; } .icon.switch[data-toggled]::after { margin-left: .15em; } .icon.switch[data-toggled]::before { box-shadow: 0 0 0 .15em currentColor, .6em 0 0 0 inset currentColor; } .icon.check::after { width: .5em; height: .15em; background-color: currentColor; border-radius: .075em; transform: rotate(45deg); margin: .1em 0 0 -.5em; } .icon.check::before { width: .15em; height: 1em; background-color: currentColor; border-radius: .075em; transform: rotate(45deg); margin: -.5em 0 0 .1em; } .icon.check[data-toggled]::after { width: 1em; margin: -.075em 0 0 -.5em; } .icon.check[data-toggled]::before { margin: -.5em 0 0 -.075em; } .icon.sound::after { width: .15em; height: .3em; margin: -.3em 0 0 0; border-radius: 0 .45em .45em 0; border: .15em solid transparent; border-left-width: 0; box-shadow: 0 0 0 .15em inset currentColor, .10em 0 0 .05em currentColor; } .icon.sound::before { width: .2em; height: .3em; margin: -.35em 0 0 -.45em; border: .2em solid transparent; border-left-width: 0; border-right-color: currentColor; box-shadow: 0 0 0 .2em inset currentColor; } .icon.sound[data-toggled]::after { width: 0; height: 0; margin: -.15em 0 0 .2em; box-shadow: 0 0 0 .15em inset currentColor, 0 0 0 0 currentColor; } .icon.sound[data-toggled]::before { margin: -.35em 0 0 -.25em; } .icon.timer::before { width: .2em; height: .2em; margin: -.3em 0 0 -.3em; border: .2em solid transparent; border-radius: .3em; box-shadow: 0 0 0 .2em inset currentColor, 0 0 0 .15em currentColor, .3em -.3em 0 -.2em currentColor, 0 -.45em 0 -.2em currentColor; } .icon.timer::after { width: .1em; height: .4em; margin: -.25em 0 0 -.05em; background-color: currentColor; border-radius: .05em; transform-origin: .05em .25em; } .icon.timer[data-toggled]::before { animation: .8s ease 5s infinite timerBlink; } .icon.timer[data-toggled]::after { animation: 5s linear 0s 1 timerRotate, .8s ease 5s infinite timerBlink; } @keyframes timerRotate { from { transform: none; } to { transform: rotate(360deg); } } @keyframes timerBlink { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } } .icon.play::after { width: 0; height: .346em; margin: -.346em 0 0 -.3em; border: .173em solid transparent; border-left: .3em solid currentColor; border-right-width: 0; } .icon.play::before { width: 0; height: 0; margin: -.173em 0 0 0; border: .173em solid transparent; border-left: .3em solid currentColor; border-right-width: 0; } .icon.play[data-toggled]::after { height: .693em; margin: -.346em 0 0 -.28em; border: 0 solid transparent; border-left: .2em solid currentColor; } .icon.play[data-toggled]::before { height: .693em; margin: -.346em 0 0 .08em; border: 0 solid transparent; border-left: .2em solid currentColor; } /* putting the buttons in a nice centered grid */ body { height: 100%; width: 100%; margin: 0; display: grid; grid-template-rows: 1fr auto auto auto 1fr; grid-template-columns: 1fr auto auto auto 1fr; grid-template-areas: ". . . . ." ". b1 b2 b3 ." ". b4 b5 b6 ." ". b7 b8 b9 ." ". . . . ."; } button:nth-child(1){ grid-area: b1; } button:nth-child(2){ grid-area: b2; } button:nth-child(3){ grid-area: b3; } button:nth-child(4){ grid-area: b4; } button:nth-child(5){ grid-area: b5; } button:nth-child(6){ grid-area: b6; } button:nth-child(7){ grid-area: b7; } button:nth-child(8){ grid-area: b8; } button:nth-child(9){ grid-area: b9; }
const body = document.querySelector('body'); const icons = document.querySelectorAll('.icon'); Array.from(icons).forEach(icon => { icon.addEventListener('click', () => { if(icon.hasAttribute('data-toggled')){ icon.removeAttribute('data-toggled'); } else { icon.setAttribute('data-toggled', ''); } }); });

Check the CSS and see if you understand how they work! And again, with the element itself included, we can do some truly amazing things. Also, these icons, just like the menu icon we made earlier, scale with font size and can be colored with the color property (much like icon fonts).

Conclusion

I hope the above made you see why pure CSS icons are awesome. To be fair, it is very situational when you want to use them and when you don't, but if you want smoothly animatable icons, this is the way to go. And even if you don't need them to be animatable, this is still a really good option. Have fun creating your own icons!