Safer Students. Better Learning.

An odyssey to find a sustainable icon system with SVGs in React

James Hycner James Hycner Jul 1, 2016 8:11:46 AM

svg-blogpost.jpg

At GoGuardian we recently started revising our internal UI library. Part of this process was auditing each React component, and during this process I realized that this would be a good time to switch from using an icon font to SVGs.

One of my requirements was that when Design wanted to change which icons were used, that it would have the least possible interaction from me. Ideally I wanted to allow designers to send pull requests that add / remove individual SVGs from a folder, and have the only other step be to publish a new version to our private npm.

I searched online for a bit but didn’t see any examples of anyone using quite the setup I desired, so here’s to getting another approach out there!

Prerequisite

There are a few background knowledge assumptions that will be made in the interest of brevity and focus:

  • Knowledge of using Node & npm as a build system
  • Knowledge of React
  • Working knowledge of Webpack

Ye Olde Setup

To get a sense of where we are going, we must first know where we are. Let’s take a look at what our starting icon setup looked like.

The Icons

All of the icons are handled black-box style by Design. They update stuff somehow, and eventually I get handed a new icon font & accompanying CSS to integrate.

Icon Component

As a developer consuming our lib, you import the Icon component from our library and do something like this:

<Icon value=”apple” className="your-custom-class" />

The value prop controls which icon is displayed, and the className prop is used to pass in a CSS class (that the user manages) to change the size/color/etc.

Here is what the Icon component looks like under the hood:

import React, {PropTypes} from 'react';
import classNames from 'classnames';

import style from '../../styles/_icons.scss';

export default function Icon (props) {
const {value, className, ...more} = props;

const iconClass = classNames(
style[value],
className
);

return (
<i
className={iconClass}
{...more}
/>
);
}

Icon.propTypes = {
className: PropTypes.string,
value: PropTypes.string.isRequired
};
 

We are using css-modules, so this line at the top is importing the css for all of the icons:

import style from '../../styles/_icons.scss';

The style object contains all the class names inside of _icons.scss, so we just can just use the keys to grab the right class. This also is handy in our documentation as we can just Object.keys(style).map() to display all of the available icons. No having to update docs every time something changes.

Object spreading all the unknown props into the more variable is to pass through all the other attributes a developer could want on the element:

const { value, className, …more } = props;

<i
className={iconClass}
{...more}
/>
 

The Upgrade

Let’s step through our preliminary moves and then set up our new system.

The Icons

So if we’re going to be switching from an icon font to SVGs then it seems the first task is to actually get the SVGs into the project.

After talking with Design I was able to get all the icons in SVG format. Here is an example of their structure: 

<svg xmlns=”http://www.w3.org/2000/svg" width=”24" height=”24" viewBox=”0 0 24 24">
<g stroke=”#000" stroke-linejoin=”round” stroke-miterlimit=”10" fill=”none”>
<path d=”M12 22.5c1.5 0 .5 1 3.5 1s6–6 6–10–2.5–7–5.5–7–3 1–4 1–1–1–4–1–5.5 3–5.5 7 3 10 6 10 2–1 3.5–1z”/>
<path stroke-linecap=”round” d=”M12 7.5v-2c0–1.1-.9–2–2–2h-2 "/>
<path d=”M14 5c2.485 0 4.5–2.014 4.5–4.5–2.485 0–4.5 2.016–4.5 4.5z”/>
</g>
</svg>

Now the main problem with the current setup is at the <svg> tag level. Previously with an icon font we were able to just set a font size with CSS and have the font be sized. Here the width & height are hard-coded so we’ll have no such luck. We’re going to pull out the <svg> tag from SVGs completely, leaving the above with this structure:

<g stroke-linejoin=”round” stroke-miterlimit=”10" fill=”none”>
<path d=”M12 22.5c1.5 0 .5 1 3.5 1s6–6 6–10–2.5–7–5.5–7–3 1–4 1–1–1–4–1–5.5 3–5.5 7 3 10 6 10 2–1 3.5–1z”/>
<path stroke-linecap=”round” d=”M12 7.5v-2c0–1.1-.9–2–2–2h-2 "/>
<path d=”M14 5c2.485 0 4.5–2.014 4.5–4.5–2.485 0–4.5 2.016–4.5 4.5z”/>
</g>

Infrastructure for Accessing the Icons

Now how will we get a list of all the SVGs anymore? We will no longer be using css-modules here because there is no CSS to reference (we only have the raw SVGs in a folder).

To accomplish this we’ll make a new file called _getIcons that is going to handle getting this list:

let fileList = require.context('_style/icons', true, /[\s\S]*$/);

let dictionary = {};
fileList.keys().forEach(x => {
x = x.replace('./', '');
dictionary[x.replace('.svg', '')] = require(`_style/icons/${x}`);
});

export default dictionary

To read all of the files in the SVGs folder we’re using something special that Webpack gives us, require.context(). This will give us all the relative paths to files in a folder that match a certain regex.

Then we make the dictionary object so that we can easily reference the SVGs by key. First we clean up the string so we just have the filename. Then we set the key to be the filename, and have the value be the inlined SVG. To get the SVG inline we’ll use svg-inline-loader as a plugin for Webpack. After we npm install the package we’ll need to add the new loader to the webpack config’s loaders section (there’s more to these sections, but they have been trimmed to focus on the important parts):

resolve: {
alias: {
'_style': path.join(__dirname, 'src/style')
}
},
module: {
loaders: [
{
test: /\.svg$/,
loader: 'svg-inline'
}
]
}

The other Webpack specific thing to note here is the alias for _style. This allows us to not have to do relative paths such as ../../style/icons, but rather lets us just import _style/icons from any folder.

Icon Component

Now that we can access the icons again, let’s take a look at the updated Icon component:

import React, {PropTypes} from 'react';

import icons from './_getIcons';
import colors from '_style/colors';

export default function Icon (props) {
let {color, size, value, ...more} = props;
delete more.className;
delete more.style;

let divStyle = {
display: 'inline-block',
height: size,
width: size
};

let svgStyle = {};
if (value.includes('filled')) {
svgStyle.fill = color;
} else {
svgStyle.stroke = color;
}

return (
<div style={divStyle}>
<svg
{...more}
viewBox="0 0 24 24"
style={svgStyle}
dangerouslySetInnerHTML={{__html: icons[value]}}
/>
</div>
);
}

Icon.propTypes = {
color: PropTypes.string,
size: PropTypes.number,
value: PropTypes.string.isRequired
};

Icon.defaultProps = {
color: colors.davysGray,
size: 24
};

Ok, let’s dive into the changes!

To start we’ll notice two new imports at the top:

import icons from './_getIcons';
import colors from '_style/colors';

The first is our new _getIcons function we just wrote. The second _style/colors is simply a large JSON object that contains hex color codes.

In this upgrade we’re forcing consumers of Icon to style it via available props rather than via CSS. To support this we scrub any styling that the user may be trying to pass in:

delete more.className;
delete more.style;

Now that we’re in SVG Land we now need to be concerned about whether our icons are hollow (stroke), or are filled in (fill). This distinction has been made in the filename by having -filled on the end for icons that are filled in:

let svgStyle = {};
if (value.includes('filled')) {
svgStyle.fill = color;
} else {
svgStyle.stroke = color;
}

To be able to set the sizing of the SVG, we’re wrapping it in a <div> that will get the actual sizing. The SVG inside of it will then expand to fill the <div>. Initially I tried doing everything via width, height, and viewBox on the <svg> itself, but this proved to not work very well.

When we gutted all the SVGs for their <svg> tag, we did so to put it here:

<svg
{...more}
viewBox="0 0 24 24"
style={svgStyle}
dangerouslySetInnerHTML={{__html: icons[value]}}
/>

We still apply any custom developer attributes via more, as long as they are not CSS-related. Visual consistency is kept by viewBox, which specifies offset and dimensions. Stroke color for the outline is specified by inlining the CSS via style.

We Done Did It

All done!

This new system has all the functionality of our old system, and is much easier for Design to make tweaks on their own.

Want to go on some odysseys yourself? We’re hiring.

Guest Writer Application

×