What’s the Gist?
- Source icons are gathered in individual SVG files in a dedicated folder.
- A sprite is generated with spritesh.
- The sprite is included in the main layout for future reference.
- Icons are displayed through a small component.
- ???
- Profit.
Gathering Icon Files
viewBox
issues…
<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>
! 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
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.
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 element (and its
viewBox
attribute) from our source files, theviewbox
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>
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 tag inside an
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 usedangerouslySetInnerHTML
. 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
and
respectively to the
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 SVGOto 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!
No comments: