The hitchhiker's guide to reusable Icons and SVG sprite generation
Tags: svg, sprites, icons, a11y
Published: 2021-04-20 16:59:16

Tell me if this sounds familiar: You have a bunch of icons to use in your awesome web project. You don't want to use an icon library (Font awesome, material icons) probably because they're from different sources, or you just want to avoid the huge size that comes with libraries. So you head to icomoon.io (or similar) and upload all of your icons and then generate and download the icon-font set (or SVG sprite) generated for you and you go ahead to use that in your project.

The problem is, in 2 weeks' time, your designer comes along with a bunch of other icons you have to add and you have to upload these new ones, a JSON file from the one you generated earlier, and then generate a new set. This can get pretty monotonous and annoying, and there are a couple of ways things can go wrong; oh and did I mention, boring and monotonous.

In this article, we're going to look at:
- how we can automate that process. Such that, whenever you need to add a new icon, you just run a command and your new SVG Sprite is generated for you
- creating a reusable component to use the generated icons in the SVG Sprite.

> This article shows how to create reusable SVG icons using SVG sprites (you should be using (controversially) SVG sprites as opposed to icon fonts. Here's why: https://css-tricks.com/icon-fonts-vs-svg/, https://www.sitepoint.com/icon-fonts-vs-svg-debate/). However, the same principles can be applied to generating and using icon-fonts as well.

## Creating an SVG sprite
The approach we’ll be using in this article allows you to easily add any new SVGs to an `icons` folder, run a script to generate your sprite, and create a reusable component to make these icons easy to use. This approach also allows you to version control your icons and modify or roll back as you need.

1. Get a bunch of icons in an svg folder

Let’s create a `src/icons` - or whatever fits your project structure - folder where we will place all our SVG icons.
![Folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xhjn3ii5hyo67yt70xvt.png)

2. Create the SVG sprite

Next, to create the svg sprite, we need to install `grunt` and `grunt-svgstore`.

```bash
yarn add --dev grunt grunt-svgstore
```

Now, to create our grunt task, create a Gruntfile.js file in the root folder of your project and put the following contents in it

```javascript
module.exports = function (grunt) {
  // Project configuration.
  grunt.initConfig({
    // PLUGINS CONFIG
    svgstore: {
      options: {
        prefix: '', // This will prefix each <g> ID in the generated SVG. You do not need it, but you can add it to avoid conflicting icon names with any other SVG sprites
        includedemo: true, // Do you want to generate a demo html file showing how the icons are used?
      },
      default: {
        files: {
          // destination_file (for the sprite): source_files (array of matchers pointing to your svg files)
          'src/assets/svgs/icon-sprite.svg': ['src/icons/*.svg'],
        },
      },
    },
  });

  grunt.loadNpmTasks('grunt-svgstore');

  // Default task(s).
  grunt.registerTask('default', ['svgstore']);
};
```
We have registered the `svgstore` task as default, hence running `grunt` should run the `svgstore` task.

> **NOTE:** The destination file is the generated SVG file path, we have set this to `src/assets/svgs/icon-sprite.svg`. Feel free to change this to any valid path in your code where you feel more comfortable have the sprite generated. The **source_files** regex matches all svg files in the `src/icons` folder we created earlier

Check the [grunt-svgstore](https://github.com/FWeinb/grunt-svgstore) documentation for more configuration options.

Finally, add the script to your package.json

```json
...
"scripts": {
  ...
  "generate-sprite": "grunt"
}
```

Now you can run `yarn generate-sprite` to generate your svg sprite. A demo HTML file will also be generated alongside and you can open up the file to make sure that every icon looks right.

### Inserting the SVG Sprite

To use our newly generated sprite, we need to insert it in the `<body>` tag. We want to insert it as the uppermost item in the DOM. 

One way to insert this would be to copy the entire contents of `custom-icons.svg` and paste it in your index.html or layout component. But this wouldn’t exactly be ideal, now would it (especially if you’re using React or Vue or any other component-based framework that uses _webpack_ to load files). 

An alternative, more ideal approach would be to use webpack to load the SVG file as a component. 
**Using Vue:** simply install and configure [`vue-svg-loader`](https://www.npmjs.com/package/vue-svg-loader). Then create the CustomSprite component.

```vue
<template>
  <div style="display: none;">
    <icon-sprite />
  </div>
</template>

<script>
import IconSprite from 'assets/svgs/icon-sprite.svg';

export default {
  components: {
    IconSprite,
  },
};
</script>
```
**Using React:** if your project is built with create-react-app and you have not yet ejected, then you don’t need to install anything, simply create the component below.

If your project has already been ejected, or you’re using `NextJs` or something similar that allows you to configure webpack, you might want to take a look at [`@svgr/webpack`](https://react-svgr.com/docs/webpack/).

Create the CustomSprite component like such:

```jsx
import React from 'react';
import { ReactComponent as IconSprite } from 'assets/svgs/icon-sprite.svg';

const CustomSprite = () => {
  return (
    <div style={{ display: 'none' }}>
      <IconSprite />
    </div>
  );
};

export default CustomSprite;
```
Note that the component is wrapped in a hidden div (and it is styled inline). That’s because we don’t want to show the sprite on the page at all (the inline styling is to prevent a flash before css loads).

Now you can insert your CustomSprite component in the root of the page

```html
<body>
  // your custom icon sprite
  <custom-sprite />
  ...
```

### Using individual icons from the sprite.
Now we need to use our newly minted icon sprite to add any icons on the page.

```html
<svg
  class="icon"
  aria-label="Rotate left"
>
  <use xlink:href="#rotate-left" />
</svg>
```

We're almost there! But this seems like an awful lot of code to write just to use a rotate-left icon. So we're going to make it into a component instead.

### Create a reusable SvgIcon component

**Vue:**
```vue
<template>
<svg
  class="icon"
  :aria-label="label"
>
  <use :xlink:href="`#${icon}`" />
</svg>
</template>
<script>
export default {
  props: {
    icon: {
      type: String,
      required: true
    },
    label: {
      type: String,
      default: null
    },
  },
};
</script>
```

**React**
```jsx
import React from 'react';
const SvgIcon = ({icon, label}) => {
  return (
    <svg
      class="icon"
      aria-label={label}
    >
      <use xlink:href={`#${icon}`} />
    </svg>
  );
};

export default SvgIcon;
```

That's it! You can now use your icons by using your component with the icon name anywhere.

```vue
<svg-icon icon="rotate-left" label="rotate-left icon" />
```
Or React

```jsx
<SvgIcon icon="rotate-left" label="rotate-left icon" />
```

Feel free to modify the process to suit your needs. Now when you have new icons, all you have to do is  

* Add them to your `src/icons` folder
* Run `npm run custom-icons` or `yarn custom-icons`

and your new icons will be added to the sprite.

I hope this helps, if you run into any troubles, please feel free to let me know.