How to build your own icon sprites

Leveraging a library like Font Awesome can be an easy way to add icons to your site. BUT - there's no way you'll need every icon on every site. A better option is to build a sprite of icons and use only what you need.

I've used Icomoon.io in the past to build icon sprites and icon fonts.

It's great. You should definitely use it.

For the CMS we've recently built, we wanted to have a little bit more control over how and when we built our icon sprites. I put together a few scripts that help us build what we need.

Tooling

If you search NPM for "SVG icons" or "SVG sprites," you'll find a number of results. A lot of the packages are for compressing or cleaning up SVG files. Some packages are for compiling multiple SVGs into one file. From what I could find, nothing actually did what we wanted, which was simply:

  1. Take a list of icons we want included in the sprite
  2. Build the final sprite

In the end, we decided to use the SVG Sprite package which warns:

Being a low-level library with support for Node.js streams, svg-sprite doesn't take on the part of accessing the file system (i.e. reading the source SVGs from and writing the sprites and CSS files to disk).

Therefore, we needed to do just a little work on our own. The readme has a ton of information about what to do but it was way too overwhelming. Here's a simpler solution.

Copying icon files

In the end, SVG Sprite will take a set of icons and generate the final sprite. Using a small script, we can copy files from Font Awesome based on a list of icons we need. We copy the Font Awesome SVGs into a temporary folder that svg-sprite can process.

First, we need a list of icons we can load. Here's our icon list (stored as icon-list.json file):

[
  "svgs/solid/angle-down.svg",
  "svgs/solid/baby.svg",
  ...
  "svgs/brands/facebook-square.svg",
  "svgs/brands/instagram-square.svg",
  "svgs/brands/linkedin.svg",
  "svgs/brands/youtube-square.svg",
  "svgs/brands/twitter-square.svg",
]
icon-list.json

Next, we need to get these icons into a temporary directory that svg-sprite can read from. In our case, the list of icons can change, so we make sure we have a clean directory before copying files from Font Awesome.

async function deleteFiles() {
  try {
    await fs.emptyDir(destDir)
  } catch (err) {
    console.error(err)
  }
}
The function for cleaning out a directory using the fs-extra package.

Once our temporary directory is empty, we can copy each SVG from Font Awesome into the directory that svg-sprite will read from.

async function copyFiles() {
  console.log("Copying icons into icons folder")

  iconFiles.forEach((file) => {
    const src = path.join(srcDir, file)
    const dest = path.join(destDir, path.basename(file))
    try {
      fs.copySync(src, dest)
    } catch (err) {
      console.error(err)
    }
  })
}
The function for copying files into a temporary directory.

In the end, our full script looks like this:

const fs = require("fs-extra")
const path = require("path")

// Get a reference to our temporary directory
const destDir = path.join(__dirname, "icons")

// Get a reference to the directory where the original Font Awesome SVGs are stored
const srcDir = path.join(
  __dirname,
  "node_modules",
  "@fortawesome",
  "fontawesome-pro"
)

// Make sure our temp directory is clean
async function deleteFiles() {
  try {
    await fs.emptyDir(destDir)
  } catch (err) {
    console.error(err)
  }
}

// Copy files from Font Awesome
async function copyFiles() {
  console.log("Copying icons into icons folder")
  
  // Grab a list of files we want in this sprite
  const iconFiles = require("./src/icons-list")

  iconFiles.forEach((file) => {
    const src = path.join(srcDir, file)
    const dest = path.join(destDir, path.basename(file))
    try {
      fs.copySync(src, dest)
    } catch (err) {
      console.error(err)
    }
  })
}

// Run our steps
;(async () => {
  await deleteFiles()
  await copyFiles()
})()

SVG Sprite Configuration

Now that we have a folder full of icons, we need to tell svg-sprite to do its thing. You can set all kinds of options - but the quickest way to get started is to use the online generator. Our final configuration is pretty simple and looks like this:

{
  "shape": {
    "dimension": {
      "maxWidth": 512,
      "maxHeight": 512
    }
  },
  "svg": {
    "xmlDeclaration": false,
    "doctypeDeclaration": false,
    "namespaceIDs": false
  },
  "mode": {
    "defs": {
      "prefix": "icon-%s",
      "sprite": "icons.svg",
      "inline": true,
      "example": false
    }
  }
}

The biggest note here is that we're using the mode property to create a defs sprite. With this configuration, we can use the command line to generate our final sprite. Assuming you have your local node_modules/.bin folder on your path, you can run:

svg-sprite --config ./src/config.json ./icons/*.svg

Or - better yet - we can just run this from npm. Which is what we do. In our package.json we define a few scripts for each step of the process:

  "scripts": {
    "copy-files": "npx ./copy-files.js",
    "create-sprite": "svg-sprite --config ./src/config.json ./icons/*.svg",
    "build": "npm run copy-files && npm run create-sprite"
  }

Final Thoughts

In the end, all this work can probably be accomplished in a simpler way (like via Icomoon). But for us, we wanted a relatively easy way to regenerate the icon sprite without having to use a third-party app. The process of selecting and updating our icon list in Icomoon is actually pretty easy - but running npm build is even easier. 😆