Skip To Content

Roll Your Own SVG Sprite Sheets With Bower and Gulp

In this article I will show you how to integrate a custom set of open source icons into a project using Bower (a front-end package manager) and Gulp (a command line task runner). This isn’t exactly the sort of tutorial where you can copy and paste a bunch of code and get up and running—it is more a collection of working notes and scraps of code meant to point you in the right direction. I will assume readers have a basic understanding of modern front-end development tools and the command line. I happen to be using WordPress and PHP to implement the markup but also include some vanilla JavaScript that could be adapted for use pretty much anywhere.

There’s an incredible wealth of information available about icon fonts, SVGs, and the like, almost too much if you’re interested in getting up and running fast. The general consensus these days is that icon fonts were a useful hack but SVGs are now ready for prime-time if you aren’t overly concerned with legacy browser support (particularly IE, as always) and don’t mind handling the usual assortment of quirks with polyfills and workarounds.

With this in mind what I set out to do is integrate individual SVG graphics into my main WordPress theme without having to load an entire set of icons or the bulky bundled scripts and stylesheets that often accompany them. I also set out to minimize payload size and HTTP requests through the use of sprites while maintaining the ability to style individual graphics at the document level (i.e. within my main stylesheet). My ultimate goal was to have a simple workflow in place to add graphical icons to a project, style them, and generate the appropriate markup without complications. Finally, I set out to automate most of this process using Bower and Gulp (though you could probably do the same with Grunt and/or npm should you prefer).

State of the art

Consider some of the approaches discussed in this great overview of SVG sprite creation techniques. Reading this led me to experiment with in-line SVG sprites, storing individual graphics in an optimized sprite sheet that is injected into the document itself. (SVG is based on XML so you can readily mix SVG source code with HTML.) Graphics are then displayed with a little bit of markup that reference an ID in the main sprite sheet—and styling is a snap since everything is now part of the DOM (or close enough anyway):

<svg class="icon">
  <use xlink:href="#icon-edit"></use>
</svg>

There is one big “gotcha” when using in-line SVG graphics: the sprite sheet file won’t be cached if it is injected directly into the document—and no version of Internet Explorer supports the use syntax with external SVG sprite sheets. There is a workaround, however: SVG For Everybody, a tiny (less than 2 Kb) JavaScript polyfill that does a little browser sniffing and injects the requested SVG file with a bit of AJAX magic where necessary. If you’re hoping to support IE versions less than 9 you’ll have to include the script in the head to polyfill the svg element itself. Annoying, I know, but TANSTAAFL!

To summarize the trade-offs between external and in-line SVG sprite sheets: cross-browser support for external sprite sheets require JavaScript and possibly an extra request; in-line SVG sprite sheets can’t be cached but support is widespread. My prescription: use the polyfill unless you’re only dealing with a handful of small icons and the final payload size is negligible. The rest of this article will discuss how to automate an icon acquisition and integration workflow with only IE9+ support in mind.

Return to the source

So, how do we automate all of this with Bower and Gulp? For starters we need raw source material: individual icons in SVG format. While there are countless icon fonts and SVG icon sets out there not all of them are readily adapted to rolling our own sprites. I had a look on Bower and found the following icon sets with SVG source files available (in alphabetical order with links to GitHub and Bower ID):

Project Source Bower ID Grid Size
Elusive GitHub elusive-iconfont 1200
Emojione GitHub emojione 64
Font Awesome GitHub font-awesome-svg-png 2048
Foundation Icon Fonts 3 GitHub foundation-icon-fonts 100
Ionicons GitHub ionicons 512
Material Design Icons GitHub material-design-icons 18/24/36/48
Octicons GitHub octicons varies
Open Iconic GitHub open-iconic 8
Typicons GitHub typicons 24

Installing icon sets with Bower is easy; just run bower install -D [name]. There’s nothing more to it.

Automation

Now to automate most things with Gulp! Recently I have been experimenting with some of the techniques outlined in gulp-starter: a modular architecture and a separation of plumbing and configuration. (If you aren’t familiar with Gulp you’re probably going to need to Google some introductory articles to get up to speed.) Have a glance at the configuration file example to get a sense of how things are structured or browse the gulpfile.js folder in Pendrell to see a full working implementation. For this example run npm install --save-dev gulp-cheerio gulp-imagemin gulp-svgstore to install the necessary Gulp plugins. You won’t necessarily need all these plugins depending on what approach you take but removing them later on is easy enough.

A quick explanation of what all these plugins are for:

  • gulp-imagemin implements svgo to compress SVG files.
  • gulp-cheerio manipulates XML data. In this case we’ll be stripping out any existing fills to allow for greater control through CSS.
  • gulp-svgstore is the workhorse that packs all the SVGs into a sprite sheet.

A simplified excerpt from my configuration file (which is then accessed in my icons.js task file with var config = require('../../gulpconfig').bower):

var bower = './bower_components/';
var src = './src/';

module.exports = {
  icons: {
    dest: src+'svg/'
  , ionicons: {
      src: bower+'ionicons/src/'
    , prefix: 'ion-'
    , icons: [
        'edit'
      , 'image'
      , 'images'
      ]
    }
  , typicons: {
      src: bower+'typicons.font/src/svg/'
    , prefix: 'typ-'
    , icons: [
        'edit'
      , 'link'
      , 'rss'
      ]
    }
  }
}

Each icon set defined in the configuration file has four properties: a source (path to the SVG source files, likely under bower_components), a destination (the same for all icon sets), a prefix (to namespace icons and avoid collisions), and an array of icon filenames (which might differ from the identifier listed on an icon set’s homepage; you’ll have to check for this yourself). A few things to note about this configuration style:

  • You can mix icons from different icon sets in your projects. Mixing icons has the potential to be a bad practice (particularly when mixing icons with different grid sizes) but there are also many legitimate uses for such functionality, from adding original content to the master icon sheet to sourcing specialized social sharing icons from a dedicated icon set. This also makes it easy to inject your own custom icons.
  • Each icon filename is on its own line to improve the workflow with Git (i.e. to make it easy to add or remove icons per branch).
  • Adding or removing icons from your project is as simple as changing a line of text in the configuration file and running gulp bower-icons.

Here is a simplified excerpt from my icons.js task file:

var gulp = require('gulp');
var plugins = require('gulp-load-plugins')({ camelize: true });
var config = require('../../gulpconfig').icons;

gulp.task('icons', ['icons-ion']);

gulp.task('icons-ion', function() {
  var iconset = config.ionicons;
  iconset.icons.forEach(function(icon, i, icons) {
    icons[i] = iconset.src+icon+'.svg';
  });
  return gulp.src(iconset.icons)
  .pipe(plugins.rename({ prefix: iconset.prefix }))
  .pipe(plugins.changed(config.dest))
  .pipe(plugins.imagemin())
  .pipe(gulp.dest(config.dest));
});

The icons task optimizes the source icons specified in the configuration file with gulp-imagemin (which takes over from the now deprecated gulp-svgo) before copying them to a staging area. Each icon set presently requires its own dedicated task—but this task is easily replicated: copy and rename the task, change the value of iconset, and add the new task to the icons dependencies array.

Material Design Icons is a special case thanks to a complicated directory structure. I handled this by defining this icon set as an array of arrays in the format [ group, icon ] to avoid long delays with glob matching (e.g. [ 'action', 'ic_account_circle_18px' ]). The task above, modified for use with Material Design Icons (requires gulp-flatten):

gulp.task('icons-material', function() {
  var iconset = config.iconsets.material;
  iconset.icons.forEach(function(icon, i, icons) {
    icons[i] = iconset.src+icon[0]+'/svg/production/'+icon[1]+'.svg'; // Differs!
  });
  return gulp.src(iconset.icons)
  .pipe(plugins.flatten())
  .pipe(plugins.rename(function(path) { path.basename = iconset.prefix + path.basename.replace(/_/g, '-') })) // Standardize filenames
  .pipe(plugins.changed(config.dest))
  .pipe(plugins.imagemin())
  .pipe(gulp.dest(config.dest));
});

Since copying and optimizing icons is resource-intensive I don’t include the icons task in my default Gulp task chain. Instead, I trigger it manually after modifying the configuration or automatically when building a new package for distribution. It is up to you how to integrate this into your own workflow.

Now let’s take a look at the SVG-related part of the configuration file (again, somewhat simplified):

var src = './src/';
var build = './build/';

module.exports = {
  svg: {
    src: src+'icons/**/*.svg' // Note that the containing folder will be used to generate the resulting filename e.g. `icons/*.svg` yields `img/icons.svg`
  , dest: build+'img/'
  , transform: {
      before: {
        run: function ($) {
          $('[fill]').removeAttr('fill'); // Remove fill attribute to allow total CSS control
        },
        parserOptions: { xmlMode: true }
      }
    }
  }
}

Now for the SVG task file itself:

var gulp = require('gulp');
var plugins = require('gulp-load-plugins')({ camelize: true });
var config = require('../../gulpconfig').svg;

// Generate an optimized SVG sprite sheet file from individual SVG source icons
// Note: IE support requires svg4everybody; even then it's only IE9+ without fallback PNGs not generated here
gulp.task('svg', function() {
  return gulp.src(config.src)
  .pipe(plugins.cheerio(config.transform.before))
  .pipe(plugins.svgstore())
  .pipe(gulp.dest(config.dest));
});

Here I use gulp-cheerio to transform the SVG output, removing any fills, before shipping everything off to gulp-svgstore, which assembles the sprite sheet based on the name of the directory containing source SVGs. (You can also use gulp-svg2png to create the PNG fallbacks necessary when supporting legacy versions of Internet Explorer with SVG For Everybody. gulp-svgfallback is another option if you’d like to generate an actual PNG sprite sheet as opposed to individual files. If you plan to use external sprite maps and don’t care about older versions of IE then all you need is gulp-svgstore and the svg-store task above.)

With all these pieces in place it should be possible to run gulp bower-icons and then gulp svg to generate the SVG sprite sheet itself. Want to add a new icon to your project? Add a line to your configuration file and run those tasks again.

Invocation

Now all you have to do is integrate the sprite sheet into your project. In the interest of providing a complete example I will demonstrate how I accomplished this both on the server (with WordPress and PHP) and on the client side (with a bit of JavaScript for those really hard to reach places).

Here is our desired markup pattern for use with an external SVG sprite sheet (complete with several SVG accessibility best practices):

<svg class="icon icon-ion icon-ion-edit" role="img" aria-labelledby="title desc">
  <title id="title">Edit</title>
  <desc id="desc">Description of the edit function</desc>
  <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://www.example.com/img/icons.svg#i-ion-edit"></use>
</svg>

The markup pattern for internal SVG sprite sheets is more or less the same minus the URL preceding the fragment identifier (#) above.

Should you wish to use an internal sprite sheet you’ll need some extra code to inject the SVG markup at the top of each page. I didn’t end up following this approach but I did write some code to help those who will. Rather than share some code here allow me to direct you to Ubik SVG Icons, the WordPress component I developed to integrate SVG icons into my themes. Browse the source and you should be able to see at a glance how everything fits together.

WordPress can be rather stubborn and difficult to work with a lot of the time so I also developed a means of generating the appropriate markup with JavaScript. This way I can inject icons as needed into locations that I don’t necessarily have direct access to without going to a lot of trouble (e.g. the output of the comment_form function). You can find the code I use in this repo if you’re curious.

One additional remark about using JavaScript with WordPress: if you use an external sprite sheet, as I do, you’ll need to provision the script with the URL to the icons file through a global or by passing it in an argument. The right way of handling globals is to use the wp_localize_script function (WordPress Codex reference) but this is starting to get a bit out of scope for this article.

Ultimately all of this boils down to two easy ways of invoking icons. In PHP with Ubik SVG Icons:

echo ubik_svg_icon( 'icon-name', 'Title', 'Description' );

In jQuery with svg.icon.js:

$('selector').prepend( svgIcon( 'icon-name', 'Title', 'Description', 'url' ) );

Styling SVG icons

Finally, styling icons is a snap with this setup (if you avoid the worst of the “gotchas” anyway, which I do with this approach). Here’s an example in Sass:

svg {
  &.icon {
    height: 24px;
    width: 24px;
    display: inline-block;
    fill: currentColor; // Inherit color
    vertical-align: baseline; // Options: baseline, sub, super, text-top, text-bottom, middle, top, bottom

    // Different styling for when an icon appears in a button element
    button & {
      height: 18px;
      width: 18px;
      margin: 4px -4px 0 4px;
      filter: drop-shadow( 0 1px #eee );
      float: right;
    }
  }

  // You can also style icons by set
  &.icon-ion {}

  // Or specific namespaced icons
  &.icon-ion-edit {}

  // Need an arrow pointing in a different direction?
  &.icon-ion-arrow-up {
    transform: rotate(90deg); // Magic!
  }

  // Fix jQuery bug: http://bugs.jquery.com/ticket/11352
  use {
    pointer-events: none;
  }
}

With this approach you can use powerful CSS transforms to further customize icons without needing to play around with source files!

Conclusion

To summarize, a complete implementation of an icon asset pipeline using an external SVG sprite sheet in a WordPress theme requires the following:

  • Open source icon sets with SVG source files available via Bower.
  • Gulp tasks to optimize/copy desired icons and assemble the sprite sheet.
  • A function to output the SVG use markup (see examples in PHP and JavaScript).
  • Internal sprite sheet: a function to inject the sprite sheet at the top of each page (see example in PHP).
  • External sprite sheet: the SVG For Everybody polyfill.
  • Custom styling (see example in Sass).

All of the code referenced in this post can be found in somewhat simplified form in this gist. If you have any questions please feel welcome to ask in the comments—if there is interest in this topic I’ll do my best to respond.

Finally, I highly recommend perusing the source code for Pendrell to see how everything hangs together. Best practices and code also change and evolve over time so your best bet is to refer to the living repository rather than relying on the contents of this post (which may start to smell funny given how quickly things move in front-end development). I’ll try to make a note at the start of the post if things start to seem really outdated around here!

Write a Comment

Markdown and HTML enabled in comments.
Your email address will not be published. Required fields are marked *