Creating an SVG Icon System with React

Avatar of Sarah Drasner
Sarah Drasner on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

I recently went to Michael Jackson and Ryan Florence’s ReactJS Training. I was really excited to attend, partially because I had so many questions about SVG and React. There are a lot of bits about working with React and SVG, and especially manipulating it, that aren’t quite supported yet. One of the major gaps for me was the <use> element, as most SVG icon systems are built with <use>.

I asked Michael if he thought better support might be coming for some of these features, but he showed me a much better way of working with it, circumventing this method entirely. We’ll go over this technique so that you can get started writing scalable SVG Icon Systems in React, as well as some tricks I’d propose could work nicely, too.

Note: It’s worth saying that use support was recently improved, but I’ve noticed it’s spotty at best and there are other routing and XML issues. We’ll show you another, cleaner way here.

What is <use>?

For those not familiar how SVG icon systems are typically built, it works a little like this. The <use> element clones a copy of any other SVG shape element with the ID you reference in the xlink:href attribute, and still manipulate it without reiterating all of the path data. You may wonder why one wouldn’t just use an SVG as an <img> tag. You could, but then every icon would be an individual request and you wouldn’t have access change parts of the SVG, such as the fill color.

Using <use> allows us to keep the path data and basic appearance of our icons defined in one place so that they could be updated once and change everywhere, while still giving us the benefit of updating them on the fly.

Joni Trythall has a great article about use and SVG icons, and Chris Coyier wrote another awesome article here on CSS-Tricks as well.

Here’s a small example if you’d like to see what the markup looks like:

See the Pen bc5441283414ae5085f3c19e2fd3f7f2 by Sarah Drasner (@sdras) on CodePen.

Why bother with SVG Icons?

Some of you at this point might be wondering why we would use an SVG icon system rather than an icon font to begin with. We have our own comparison on that subject. Plus there are a ton of people writing and speaking about this right now

Here are some of the more compelling reasons, in my mind:

  • Icon fonts are hard to make accessible. SVG has the ability to add title and ARIA tags, which provide a huge boon to accessibility, particularly in cases when the icon is alone and a single source of informative navigation. Think: blind people, dyslexic people, the elderly (You will be elderly too someday, hopefully, so if you’re not the kind of dev to care about this subset, do it for the karma! But seriously, care for the elderly.)
  • Icon fonts aren’t as crisp on some displays. You can avoid this by doing some fancy font-smoothing in CSS, but here’s one caveat I’ve noticed: it’s difficult to override without turning font-smoothing off entirely. SVGs are more crisp in general, drawing is what they’re built for.
  • Icon fonts fail a good amount of the time. Most developers I know have run into scenarios with missing glyph X in a box, there are a lot of ways that icon fonts can fail where SVGs do not. Be it CORS problems or Opera mini, it’s a headache.
  • Icon fonts are difficult to position. They’re an image that you’re positioning with font styles. ‘Nuff said. You can’t animate pieces of them without hacky stacking. SVGs offer a navigable DOM to animate parts of an icon, or colorize sections. Not everyone would want to do this, but it sure is nice to have the option.

If you’re like me and updating an enormous codebase, where in order to move over from an icon font to SVG you’d have to update literally hundreds of instances of markup, I get it. I do. It might not be worth the time in that instance. But if you’re rewriting your views and updating them with React, it’s worth revisiting an opportunity here.

Tl;dr: You don’t need <use> in React

After Michael patiently listened to me explain how we use <use> and had me show him an example icon system, his solution was simple: it’s not really necessary.

Consider this: the only reason we were defining icons to then reuse them (usually as <symbol>s in <defs>) was so that we didn’t have to repeat ourselves and could just update the SVG paths in one spot. But React already allows for that. We simply create the component:

// Icon
const IconUmbrella = React.createClass({
 render() {
   return (
     <svg className="umbrella" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
	<title id="title">Umbrella Icon</title>
        <path d="M27 14h5c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0zM27 14c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0 14c0 1.112-0.895 2-2 2-1.112 0-2-0.896-2-2.001v-1.494c0-0.291 0.224-0.505 0.5-0.505 0.268 0 0.5 0.226 0.5 0.505v1.505c0 0.547 0.444 0.991 1 0.991 0.552 0 1-0.451 1-0.991v-14.009c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-5.415 6.671-9.825 15-9.995v-1.506c0-0.283 0.224-0.499 0.5-0.499 0.268 0 0.5 0.224 0.5 0.499v1.506c8.329 0.17 15 4.58 15 9.995h-5z"/>
      </svg>
   )
 }
});

// which makes this reusable component for other views
<IconUmbrella />

See the Pen SVG Icon in React by Sarah Drasner (@sdras) on CodePen.

And we can use it again and again, but unlike the older <use> way, we don’t have an additional HTTP request.

Two SVG-ish things you might notice from the above example. One, I don’t have this kind of output:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

Or even this on the SVG tag itself:

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" …

That’s because I’ve made certain to optimize my SVGs with SVGOMG or SVGO before adding the markup everywhere. I strongly suggest you do as well, as you can reduce the size of your SVG by a respectable amount. I usually see percentages around 30% but can go as high as 60% or more.

Another thing you may notice is I’m adding a title and ARIA tag. This is going to help screen readers speak the icon for people who are using assistive technologies.

 <svg className="umbrella" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
  <title id="title">Umbrella Icon</title>

Since this id has to be unique, we can make pass props to our instances of the icon and it will propagate to both the title and aria tag like so:

// App
const App = React.createClass({
  render() {
    return (
      <div>
        <div className="switcher">
          <IconOffice iconTitle="animatedOffice" />
        </div>
        <IconOffice iconTitle="orangeBook" bookfill="orange" bookside="#39B39B" bookfront="#76CEBD"/>
        <IconOffice iconTitle="biggerOffice" width="200" height="200"/>
      </div>
    )
  }
});

// Icon
const IconOffice = React.createClass({
 ...
 render() {
   return (
     <svg className="office" xmlns="http://www.w3.org/2000/svg" width={this.props.width} height={this.props.height} viewBox="0 0 188.5 188.5" aria-labelledby={this.props.iconTitle}>
        <title id={this.props.iconTitle}>Office With a Lamp</title>
        ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App/>, document.querySelector("#main"));

The best part, perhaps

Here’s a really cool part of this whole thing: aside from not needing additional HTTP requests, I can also completely update the shape of the SVG in the future without any need for markup changes, since the component is self-contained. Even better than that, I don’t need to load the entire icon font (or SVG sprite) on every page. With all of the icons componentized, I can use something like webpack to “opt-in” to whatever icons I need for a given view. With the weight of fonts, and particularly heavy icon font glyphs, that’s a huge possibility for a performance boon.

All of that, plus: we can mutate parts of the icon on the fly with color or animation in a very simple way with SVG and props.

Mutating it on the fly

One thing here you might have noticed is we’re not yet adjusting it on the fly, which is part of the reason we’re using SVG in the first place, right? We can declare some default props on the icon and then change them, like so:

// App
const App = React.createClass({
  render() {
    return (
      <div>
        <IconOffice />
        <IconOffice width="200" height="200"/>
      </div>
    )
  }
});

// Icon
const IconOffice = React.createClass({
  getDefaultProps() {
    return {
      width: '100',
      height: '200'
    };
  },
 render() {
   return (
     <svg className="office" width={this.props.width} height={this.props.height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 188.5 188.5" aria-labelledby="title">
        <title id="title">Office Icon</title>
        ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App />, document.querySelector("#main"));

See the Pen SVG Icon in React with default props by Sarah Drasner (@sdras) on CodePen.

Let’s take it a step further, and change out some of the appearance based on the instance. We can use props for this, and declare some default props.

I love SVG because we now have a navigable DOM, so below let’s change the color of multiple shapes on the fly with fill. Keep in mind that if you’re used to dealing with icon fonts, you’re no longer changing the color with color, but rather with fill instead. You can check the second example below to see this in action, the books have changed their color. I also love the ability to animate these pieces on the fly, below we’ve wrapped it in a div to animate it very easily with CSS (you may need to hit rerun to see the animation play):

See the Pen SVG Icon in React with default props and animation by Sarah Drasner (@sdras) on CodePen.

// App
const App = React.createClass({
  render() {
    return (
      <div>
        <div className="switcher">
          <IconOffice />
        </div>
        <IconOffice bookfill="orange" bookside="#39B39B" bookfront="#76CEBD" />
        <IconOffice width="200" height="200" />
      </div>
    )
  }
});

// Icon
const IconOffice = React.createClass({
  getDefaultProps() {
    return {
      width: '100',
      height: '200',
      bookfill: '#f77b55',
      bookside: '#353f49',
      bookfront: '#474f59'
    };
  },
 render() {
   return (
     <svg className="office" xmlns="http://www.w3.org/2000/svg" width={this.props.width} height={this.props.height} viewBox="0 0 188.5 188.5" aria-labelledby="title">
        <title id="title">Office Icon</title>
        <g className="cls-2">
          <circle id="background" className="cls-3" cx="94.2" cy="94.2" r="94.2"/>
          <path className="cls-4" d="M50.3 69.8h10.4v72.51H50.3z"/>
          <path fill={this.props.bookside} d="M50.3 77.5h10.4v57.18H50.3z"/>
          <path fill={this.props.bookfront} d="M60.7 77.5h38.9v57.19H60.7z"/>
          <path className="cls-7" d="M60.7 69.8h38.9v7.66H60.7z"/>
          <path className="cls-5" d="M60.7 134.7h38.9v7.66H60.7z"/>
          ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App />, document.querySelector("#main"));
.switcher .office {
  #bulb { animation: switch 3s 4 ease both; }
  #background { animation: fillChange 3s 4 ease both; }
}

@keyframes switch {
  50% {
    opacity: 1;
  }
}

@keyframes fillChange {
  50% {
    fill: #FFDB79;
  }
}

One of my awesome coworkers at Trulia, Mattia Toso, also recommended a really nice, much more clean way of declaring all of these props. We can reduce repetition of the this.props here by declaring const for all our uses, and then just simply apply the variable instead:

render() {
   const { height, width, bookfill, bookside, bookfront } = this.props;
   return (
     <svg className="office" xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 188.5 188.5" aria-labelledby="title">
        <title id="title">Office Icon</title>
        <g className="cls-2">
          <circle id="background" className="cls-3" cx="94.2" cy="94.2" r="94.2"/>
          <path className="cls-4" d="M50.3 69.8h10.4v72.51H50.3z"/>
          <path fill={bookside} d="M50.3 77.5h10.4v57.18H50.3z"/>
          <path fill={bookfront} d="M60.7 77.5h38.9v57.19H60.7z"/>

We can also make this even more awesome by declaring propTypes on the props we are using. PropTypes are super helpful because they are like living docs for the props we are reusing.

propTypes: {
  width: string,
  height: string,
  bookfill: string,
  bookside: string,
  bookfront: string
},

That way if we use them improperly, like in the example below, we will get a console error that won’t stop our code from running, but alerts other people we might be collaborating with (or ourselves) that we’re using props incorrectly. Here, I’m using a number instead of a string for my props.

<IconOffice bookfill={200} bookside="#39B39B" bookfront="#76CEBD" />

And I get the following error:

See the Pen SVG Icon in React with spread with error by Sarah Drasner (@sdras) on CodePen.

Even more slender with React 0.14+

In newer versions of React, we can reduce some of this cruft and simplify our code even more, but only if it’s a very “dumb” component, e.g. it doesn’t take lifecycle methods. Icons are a pretty good use case for this, since we’re mostly just rendering, so let’s try it out. We can be rid of React.createClass and write our components as simple functions. This is pretty sweet if you’ve been using JavaScript for a long time but are less familiar with React itself- it reads like the functions we’re all used to. Let’s clean up our props even further and reuse the umbrella icon just as we would on a website.

// App
function App() {
  return (
    <div>
      <Header />
      <IconUmbrella />
      <IconUmbrella umbrellafill="#333" />
      <IconUmbrella umbrellafill="#ccc" />
    </div>
  )
}
 
// Header
function Header() {
 return (
   <h3>Hello, world!</h3>
 )
}

// Icon
function IconUmbrella(props) {
  const umbrellafill = props.umbrellafill || 'orangered'
  
  return (
    <svg className="umbrella" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
      <title id="title">Umbrella</title>
      <path fill={umbrellafill} d="M27 14h5c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0zM27 14c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0 14c0 1.112-0.895 2-2 2-1.112 0-2-0.896-2-2.001v-1.494c0-0.291 0.224-0.505 0.5-0.505 0.268 0 0.5 0.226 0.5 0.505v1.505c0 0.547 0.444 0.991 1 0.991 0.552 0 1-0.451 1-0.991v-14.009c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-5.415 6.671-9.825 15-9.995v-1.506c0-0.283 0.224-0.499 0.5-0.499 0.268 0 0.5 0.224 0.5 0.499v1.506c8.329 0.17 15 4.58 15 9.995h-5z"/>
    </svg>
  )
}
 
ReactDOM.render(<App />, document.querySelector("#main"));

See the Pen SVG Icon in React by Sarah Drasner (@sdras) on CodePen.

SVG icon systems are beautifully simple and easily extendable in React, have less HTTP requests, and are easy to maintain in the future, due to the fact that we can completely update the output in the future without any repetitive markup changes. We can increase performance by opting into exactly what we need. We can change them on the fly with props for color and even add CSS animation. All of this, and we can also make them accessible for screen readers, which makes React and SVG icon systems a really nice way to add icons to views on the web.