Icons as React Components
For the developer in a hurry, I’ll start with the gist of what I will be building in this post: a self-contained Icon component that renders one of several icons.
The key takeaway is defining icons as SVG paths.
If you can spot the five or six things wrong with this component, keep reading. If you can’t spot the five or six things wrong with this component, keep reading.
When I was a lad, my father sat me down and said David, life is going to get complicated. You’ll be faced with difficult choices, and it won’t always be clear which is the right path.
So far my life has been a breeze, except when it comes to implementing icons in a website.
Icon fonts are great. But network requests are like pet turtles. Half a dozen or so is totally normal and quite necessary, but each additional one brings you closer to madness. And async’s not an option because I don’t want a FOMI (flash of missing icons).
Embedding the font and accessing it via CSS classes is fine, but with that method, I’ll have font files, @font-face rules, css files and maybe a file of sass variables. Try two devs updating that in two branches and merging together again… Also I’m not using CSS*.
Icons as .svg files are a giant pain.
And if you’re using a png spritesheet then you don’t deserve React.
And so, for Malla**, I’ve implemented icons in a way that, I think, is the right way. Here are the ‘features’ of my approach:
- Icons are just components
- Icons are rendered inline as SVG (server-side too)
- Code weight is tiny
- Adding a new icon is adding one line of code — not 7 files
- Totally independent of icon libraries if you want to make your own icons
So, how is this done?
First, a lesson in SVG
A shape, when represented in SVG, is defined by a bunch of coordinates. Here’s a trash icon:
M192 1024h640l64–704h-768zM640 128v-128h-256v128h-320v192l64–64h768l64 64v-192h-320zM576 128h-128v-64h128v64z
Imagine you want to have a React component called, say, <TrashIcon/> that renders, say, a trash icon. All you need to do is tell it to render a bit of SVG with those coordinates.
Boom. Trash icon. Seven lines of code.
(Those coordinates happen to be in the context of a 1024x1024 grid, so we use the viewbox attribute to tell the SVG element that. Also that we want the end result to be 22px.)
We’re making excellent progress, we now have a component that we can use whenever we want to see a bin on the page.
As you probably guessed, the next step is to make it so we can show more than just a bin. Let’s turn that TrashIcon component into an Icon component and let it accept a prop called icon.
I pass in the icon name as a string, the component then ‘looks it up’ in my icons object using bracket notation, which returns the path.
Lovely, we have an icon component that will show either a trash icon
<Icon icon=“trash” />
or a Facebook icon
<Icon icon=“facebook” />
Lo, the parent component was pleased that the child component would take the burden of rendering itself and declared: “props to you”.
The next question is, where do I get all those coordinates from? Personally I just type them out in the shape I want, but perhaps you want someone else to do all the work for you.
IcoMoon is the best of all the icon packages out there*** and they have a nice interface for selecting and downloading icons. What we want is just a single .json file that has all the SVG definitions for our icons. There’s a button for that, God bless ‘em.
If you’re using webpack and have a json loader already, then you can just import the .json file directly.
But if you think “json loader” is a type of truck then you can just rename the file extension to .js and add export default at the start and treat it like a module.
This is what it looks like if we just export the Facebook and trash icon from IcoMoon.
There’s heaps of junk in there for only two icons. Maybe you care, maybe you don’t.
How about you … Choose Your Own Adventure!
If you would like to just use the selection.js file as it comes from IcoMoon (with all the junk), then take “The Easy Way”.
If you want to get rid of all the junk (this is the correct option), skip to “Minimalizing”.
The Easy Way
OK, lazy pants, to use the selection.js file as it is, we’ll need to loop over the contents, so let’s add a little getPath function to do this for us.
Boom, we’re done.
If you want to add a new icon, do it in IcoMoon, export the file again and copy the new selection.json contents into selection.js. Your git history will show a relatively neat addition of a single icon in a single file, and the instructions to update are simple enough that you’ll totally put them in your README file, won’t you.
Your adventure ends here.
All that cruft in selection.js make my eyes itchy, so with a bit of fiddling, we can get just the name and the path of each icon to have one row each.
We no longer need the getPath function because we’re just looking at a simple object. So we’re back to something like at the beginning of the post.
If you want to add a new icon, you can either download a new IcoMoon package and copy the path out of selection.json. Or as I do, open up the Chrome dev tools and just copy the path right out of the DOM from IcoMoon’s website.
If you want to create a new icon in Illustrator, just make sure you create a new file with a 1024x1024 artboard, then save as > svg > preview SVG and you’ll get the path.
So that’s it. You’ll have a system that’s as fast as can be, contained in a single component, and easy to update when you add or change icons.
But… let’s get serious
As with all blog posts, the above is oversimplified. I wouldn’t use this in production.
Let’s think about this… you want a facebook icon. So you put an <Icon/> component in your code and give it a string that says ‘facebook’, so the component can go and look that string up in a list of available icons and return the actual path?
Well that’s just silly.
The correct thing to do is to have your paths as constants. So here I have all my icons in my global constants file:
So that when I want to define an icon, I put an Icon component in my markup and pass it the value as a constant. Here’s the usage in a fictional component.
This has the nice side-effect of auto-complete when you’re trying to remember the name of the icon (or are you using a toy text editor?).
So finally (hey, thanks for reading this far), this is the actual icon component I’m using for Malla. Note that I can just pass props.icon straight into the <path> element because React will give me a Prop Types warning if the icon isn’t defined.
There are a few extra things going on here:
- You can pass in a size and a color.
- Oh, there was only one extra thing.
That’s it folks, a pretty simple Icon component in one or two files that’s a snap to update.
*** Runners up in the icon wars:
- Google’s Material Icons look great and have a huge range, but they don’t have have social icons (face-what? twit-who?)
- Foundation IconFonts and FontAwesome to me just don’t look as good.
- There are many many more but none matched IcoMoon in my eyes.