HTML & CSS
Article

A Working SVG Workflow for Accessible Icons

By Hugo Giraudel

It has been commonly admitted that using fonts for icons is a bad practice. It’s bad for rendering (sizing, positioning, etc.), it has weird failures (proxy browsers, CORS, etc.), it conveys poor semantics, can hurt accessibility, and the list goes on. CSS-Tricks has an insightful piece on the matter.

That being said, I always find it a bit complex to deal with SVG. Let’s be honest, I’m no Sara Soueidan. SVG is not really my thing. All these paths, weird tags, and attributes, ugh. I don’t really know how to use them. Still, I have to. And still, I like to do things properly.

So I came up with an SVG workflow for icons that is both simple and automated. Took me a bit of time to end up here, but I’m happy to write about it now that it works pretty well. Hopefully, it will be useful to some of you.

What’s the Gist?

CSS-Tricks has a very nice article by Chris Coyier about how to build an icon system with SVG sprites. Given that this article is a direct application of Chris’s article, I suggest you read it if you haven’t already.

Here is roughly how it works.

  1. Source icons are gathered in individual SVG files in a dedicated folder.
  2. A sprite is generated with spritesh.
  3. The sprite is included in the main layout for future reference.
  4. Icons are displayed through a small component.
  5. ???
  6. Profit.

The setup depends a bit on the type of project you are working on (Jekyll, React, Rails…) but the gist of it stays the same. In this article, I’ll walk you through the steps so you can start using it today.

Gathering Icon Files

If you ask me, this is somewhat of a hard part, because having correct SVG files is not an easy topic. Depending on which tool is used to export the icon, the markup looks different, can be bloated, and so on. And don’t get me started on viewBox issues…

If you can avoid designing your own icons, I’d recommend you do that. Icomoon has a very decent collection of lovely icons that are free to use and exportable as SVG. You could not wish for more. Which is why we will go this way in this article.

Icomoon icons

First, select the icons you need. Once you are done, click on “Generate SVG & More”. You should see listed all the icons you chose. Now you can download the bundle from Icomoon. The generated zip contains everything you need for the icons you picked (PNG files, SVG files, CSS, JavaScript, demo…). I recommend you copy the content of the SVG sub-folder (containing individual icon files), and paste it into your icon folder.

Before moving on, you will have to clean up the SVG files a bit. It is not entirely necessary per se, but I’d recommend you do that nevertheless to avoid bloating your SVG sprite with a lot of junk. Here is what the fire icon from Icomoon looks like when downloaded:

<?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">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" 
     xmlns:xlink="http://www.w3.org/1999/xlink" 
width="16" height="16" viewBox="0 0 16 16">
<path d="M5.016 16c-1.066-2.219-0.498-3.49 0.321-4.688 0.897-1.312 1.129-2.61 
1.129-2.61s0.706 0.917 0.423 2.352c1.246-1.387 1.482-3.598 1.293-4.445 2.817 1.969 
4.021 6.232 2.399 9.392 8.631-4.883 2.147-12.19 1.018-13.013 0.376 0.823 0.448 
2.216-0.313 2.893-1.287-4.879-4.468-5.879-4.468-5.879 0.376 2.516-1.364 5.268-3.042 
7.324-0.059-1.003-0.122-1.696-0.649-2.656-0.118 1.823-1.511 3.309-1.889 5.135-0.511 
2.473 0.383 4.284 3.777 6.197z"></path>
</svg>

So much for a <path>! I recommend you remove the XML definition, the initial comment, the doctype, and the SVG wrapper (because we will inline SVG by referencing the sprite), so only the actual drawn content remains:

<path d="M5.016 16c-1.066-2.219-0.498-3.49 0.321-4.688 0.897-1.312 1.129-2.61 
1.129-2.61s0.706 0.917 0.423 2.352c1.246-1.387 1.482-3.598 1.293-4.445 2.817 1.969 
4.021 6.232 2.399 9.392 8.631-4.883 2.147-12.19 1.018-13.013 0.376 0.823 0.448 
2.216-0.313 2.893-1.287-4.879-4.468-5.879-4.468-5.879 0.376 2.516-1.364 5.268-3.042 
7.324-0.059-1.003-0.122-1.696-0.649-2.656-0.118 1.823-1.511 3.309-1.889 5.135-0.511 
2.473 0.383 4.284 3.777 6.197z"></path>

Generating the Sprite

You might be asking yourself why we don’t use the sprite generated by Icomoon directly (symbol-defs.svg)? Actually, we could. You’ll see that what we are about to do is basically doing the same thing. The reasons I don’t like using the sprite are:

  • Icomoon adds a few things that are not necessary (mostly attributes) and are not always relevant (e.g. <title> filled after file name).
  • I don’t want to go back to Icomoon to download the sprite again every time I want to add an icon to the sprite. It’s nice to have a system inside the project.

There are several ways to generate a sprite from a folder of icon files, but most of them rely on some kind of asset pipeline like Grunt or Gulp. This is why I created spritesh, a Bash script that does Just That™.

Note: If you have a sprite generator that you like, please by all mean stick to it. spritesh is only a small helper for when you can’t/don’t want to load Gulp/Grunt and all the dependencies just for bundling icons.

You can install spritesh through npm or as a gem (both being thin wrappers around the Bash script anyway):

npm install spritesh -g

Then, run spritesh on the folder of icons. Assuming you stored the icon files in assets/images/icons and want to generate the sprite in the _includes folder, here is how it would look:

spritesh --input assets/images/icons --output _includes/sprite.svg --viewbox "0 0 16 16" --prefix icon-

Note: You might want to put this command in an npm script to avoid retyping it every time you want to regenerate the sprite.

Since we stripped out the <svg> element (and its viewBox attribute) from our source files, the viewbox argument is necessary. Here we use 0 0 16 16 because this is what Icomoon uses in the first place.

The prefix argument is not mandatory either. It just prevents any conflict with already-existing id attributes in the DOM when including the sprite. Should not be necessary, but I’d argue it’s a good practice to namespace the id attribute of the icons.

Note: If working on Windows, you will have to run spritesh in git bash or Cygwin.

You should now have a sprite looking like this:

<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id='icon-fire' viewBox='0 0 16 16'><path d="M5.016 
16c-1.066-2.219-0.498-3.49 0.321-4.688 0.897-1.312 1.129-2.61 
1.129-2.61s0.706 0.917 0.423 2.352c1.246-1.387 1.482-3.598 
1.293-4.445 2.817 1.969 4.021 6.232 2.399 9.392 8.631-4.883 
2.147-12.19 1.018-13.013 0.376 0.823 0.448 2.216-0.313 
2.893-1.287-4.879-4.468-5.879-4.468-5.879 0.376 2.516-1.364 
5.268-3.042 7.324-0.059-1.003-0.122-1.696-0.649-2.656-0.118 
1.823-1.511 3.309-1.889 5.135-0.511 2.473 0.383 
4.284 3.777 6.197z"></path></symbol>
<!-- Other <symbol>s… -->
</svg>

The only thing left to do is include the sprite in the main layout. If working on a Jekyll website for instance, it could be as simple as generating the sprite in the _includes/ folder and adding this to the layout file:

{% include sprite.svg %}

Creating an Icon Component

So far we’ve gathered icons and made a sprite. That’s nice, however, we still have to set up a convenient way to use these sprited icons. We will use a <use> tag inside an <svg> tag to reference the relevant symbol from our sprite (more on that technique in this article on CSS-Tricks), like this:

<svg viewBox="0 0 16 16" class="icon icon-fire">
  <use xlink:href="#icon-fire"></use>
</svg>

It works, but it’s not very friendly and ends up being quite annoying to deal with in the long run. Also, if we need to change the class, or the viewBox attribute, we’ll have to go over all the occurrences of this in the project. Not ideal at all.

What we want is to abstract this bit of repetitive markup in a partial. In Jekyll, it could look like this:

<svg viewBox="0 0 16 16" class="icon icon-{{ include.icon }}">
  <use xlink:href="#icon-{{ include.icon }}"></use>
</svg>

To use it, you include the partial and pass it an icon parameter:

{% include icon.html icon="fire" %}

Feel free to improve the partial to make it accept other parameters such as an extra class.

In React, it could look like this:

const Icon = (props) => (
  <svg viewBox='0 0 16 16' className={`icon icon-${props.icon}`}>
    <use xlinkHref={`#icon-${props.icon}`} />
  </svg>
);

export default Icon;

Note: the xlinkHref is only available as per React 0.14. In React 0.13, you’ll have to use dangerouslySetInnerHTML. More on that in this answer on Stack Overflow.

And then:

<Icon icon='fire' />

A Word on Accessibility

In this article from Léonie Watson, it is recommended to add a title and a description with <title> and <desc> respectively to the <symbol> definitions in the sprite, to improve accessibility.

I fully support this, however, I tend to think that title and description are heavily dependent on context. Therefore, they are better defined at use time (in our component) rather than at definition time (in the sprite) in my opinion.

For instance, if you use an icon alongside text, you don’t want a title to be read out loud, because text is already there for this. On the other hand, if you use an icon as only content for a button, you want a title and description to be read, so the user knows the purpose of the button.

Here is how I would update our component to make it possible to pass a title and a description, making it highly accessible.

{% capture id %}{% increment uniqueid %}{% endcapture %}
<svg viewBox="0 0 16 16" role="img" class="icon icon-{{ include.icon }}" 
     aria-labelledby="{% if include.title %}title-{{ id }}{% endif %}{% if include.desc %} desc-{{ id }}{% endif %}">
  {% if include.title %}
  <title id="title-{{ id }}">{{ include.title }}</title>
  {% endif %}  
  {% if include.desc %}
  <desc id="title-{{ id }}">{{ include.desc }}</desc>
  {% endif %}
  <use xlink:href="#icon-{{ include.icon }}"></use>
</svg>

The {% increment %} Liquid tag initialises a variable and then bumps it by one every time it is being called. In our scenario, it is called every time we include the icon partial, so for every icon.

The React version would work the same, using Lodash to get a unique id (feel free to use the implementation of your choice):

import { uniqueId } from 'lodash';

const Icon = (props) => {
  const id = uniqueId();
  return (
    <svg viewBox='0 0 16 16' role='img'
    className={`icon icon-${props.icon}`}
    aria-labelledby={
      (props.title ? `title-${id}` : '') +
      (props.desc ? ` desc-${id}` : '')
    }>
      {props.title && <title id={`title-${id}`}>{props.title}</title>}
      {props.desc && <desc id={`desc-${id}`}>{props.desc}</desc>}
      <use xlinkHref={`#icon-${props.icon}`} />
    </svg>
  );
}

export default Icon;

I will concede that it is extensively more verbose, but that’s not really a problem given that:

  • The role of a component is to abstract complexity and avoid repetition.
  • Accessibility is extremely important and should be a priority.

Wrapping Things Up

Hey, this wasn’t so bad now, was it? If we sum up, our system makes it easy to:

  • Generate a sprite from the command line (which makes it easily pluggable in any build script) with custom options;
  • Use the sprite and customise the output with a partial / component;
  • Add new icons.

That sounds like a good system if you ask me! If we want to push things further, we could pipe in SVGO to optimise the SVG files. Install it via npm:

npm install svgo spritesh --save-dev

Then, make good use of npm scripts in your package.json:

{
  "scripts": {
    "sprite": "spritesh --input assets/images/icons --output _includes/sprite.svg --viewbox '0 0 16 16' --prefix icon-",
    "presprite": "svgo assets/images/icons"
  },
  "devDependencies": {
    "spritesh": "^1.0.8",
    "svgo": "^0.6.1"
  }
}

Now, everytime we run the sprite task npm will first run svgo on the icon folder (which is only useful once, but might be good to keep in case we add new icons).

There is probably room for further improvement so if you think of anything to make this SVG workflow better, please be sure to share!

Huge thanks to Sara Soueidan for her technical review on the SVG side, and to Heydon Pickering for his insightful opinion regarding accessibility.

  • Sue Bless

    With everybody’s busy itineraries, in some cases our acne treatment regimens may slip by the wayside.

  • Great article Hugo, thanks for sharing!

  • There’s also the uses cases where you don’t want the icon to be read:

    1. Icon is purely decorative.
    2. Icon complements a visible text label that is sufficient on its own (eg. <a><svg />Latest news</a>)

    In that case, you can use the aria-hidden attribute.

    I would suggest making your HTML helper (or macro or component) output aria-hidden=”true” if both the title and desc parameters (or props) are empty. The Liquid example could look like:

    {% if include.title or include.desc %}
    aria-labelledby=”{% if include.title %}title-{{ id }}{% endif %}{% if include.desc %} desc-{{ id }}{% endif %}”
    {% else %}
    aria-hidden=”true”
    {% endif %}
    (I don’t really know Liquid, so the syntax may be a bit wrong.)

  • Chris Lee

    One issue to watch out for with Icomoon: Some of the included libraries have really bad vectors, which leads to unnecessarily large paths. I’ve been able to reduce a few by nearly 80% just by removing unnecessary path points.

    Not to hate on free stuff — because the authors deserve praise for actually putting in work and giving that work out for free — but it’s worth opening those SVGs in an editor and cleaning them up before using them in production.

    • Ah! Good to know. Thanks for the heads up.

      • mike

        Great article.
        Though possibly a compression stage, for at least SVG paths, wouldn’t go amiss?

  • Mike

    Great article. Would be nice to have browser support information though, guess no IE11 (or polyfilled?). Thanks!

    • Hello. What makes you think there would be no support for IE11?

      • Mike

        guess I misread (only skimmed the article sorry), seems you are not using an external sprite (which would not work AFAIK) but inline single icon sprites which should work fine.
        oh, btw, after rereading I noticed the spritesh script. Feels strange to install a shell script via npm or as gem. node/grunt/gulp version at least would be crossplatform. Or I could wait for Win10 summer release with bash ;)

        • Agreed. It has been said to me before. Maybe I should turn it into JS, but I wanted to write a bash script.

  • This is how I do svg icons on current project:
    1) Put absolutely all svg icons in one folder. Don’t need to care about anything, designer sending you new icon? Just copy it in folder and forget.
    2) Then I’m using ‘gulp-svg-symbols’. Point on folders with icons, few settings, and I got one svg file with bunch of symbols (with viewBox and other stuff) and .css file with specific classes (%special-icon-prefix%+%svgIconName%). These specific classes in css have all default sizes for each icon. So if you just want to insert some icon, exported from sketch with same size as it is, you don’t need to do anything.
    3) Include .css file in your sass or whatever file.
    4) On front-end I’m using riot.js right now, but you can use react or whatever. You just creating very simple component with parameters like ‘icon-name’, ‘additional-classes’ and whatever. And then when I need to include icon, I’m just writing in html and component inserts svg icon pointed on our svg file, with spefic icon class and other things, whatever you need.

  • tomByrer

    I’m not sure if sprite sheets are really needed anymore because of HTTP2.

    • HTTP/2 is not supported everywhere as far as I know. :)

Recommended
Sponsors
Get the latest in Front-end, once a week, for free.