A reflection on working with an SVG icon system

Someone on Twitter recently mentioned the push back he saw from developers in regards to using an SVG icon system. Since I had just built an SVG icon system for a client's site re-design, I decided to take this opportunity and talk about the good and bad that I noticed in using entirely SVGs on a Magento store.

In our case, icons are being used across two websites and multiple locales. The site is built on Magento 1.9, although that doesn't really matter to the scope of this post. The two sites shared a base theme and then had site and locale-specific themes that overrode where necessary. After building out the icon system and using it on the site, I have come to see many benefits of using SVG over other options—primarily font icons—but there are certainly a few hurdles to cross.

I will start by covering the benefits of using SVG on this site, then the drawbacks, along with how we solved some of the problems. I will finish with information on the icon system itself and how it works. I consider myself somewhat new to some of this so please sound off in the comments if I am missing something or there's a better way!

Benefits:

Since there are many other articles detailing the benefits of using SVGs instead of icon fonts, I want to focus on the reasons why SVG worked well on the site we were developing.

Modularity:

Instead of having one large icon file, we split them out into several based on the category, and position, or importance, of the icon. This way, if a certain page didn't need any icons from a group, it wouldn't have to request that file. Also, if one of the sites had more, or a different set of icons in a category, it would not impact the set on the others. The bottom line is we send a near-minimum of icons to the user.

Maintainability:

Things change. With the SVG icon system, editing one icon is as simple as opening the source SVG and making the adjustment. It's extremely easy.

Icons can also be added to and removed from the system just by adding or removing individual SVG files from the respective folders. As a result, if an icon becomes deprecated, it can be removed by simply deleting a single file.

CSS Control

From the CSS, we control the stroke (color), stroke-width, and fill. In addition to that, it is even possible to set two different values for colors on separate elements within the icon. This is accomplished by setting the property in the SVG to have a value of currentColor. For example, in the following real example, notice the control that we have over the colors. When the .is-open class is applied (via Javascript), the icon completely transforms (inverts) colors.

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="17" viewBox="0 0 24 17">
  <g fill-rule="evenodd" transform="translate(0 1)">
    <rect width="24" height="3" y="1.5" />
    <rect width="24" height="3" y="10.5" />
    <circle cx="18" cy="3" r="3" fill="currentColor"/>
    <circle cx="7.5" cy="12" r="3" fill="currentColor" />
  </g>
</svg>

SCSS (slightly adjusted for clarity):

.filter-icon {
  color: $white;
  fill: $grid-action-color;
  stroke: $grid-action-color;

  .is-open & {
    color: $grid-action-color;
    fill: $white;
    stroke: $white;
  }
}

No Fallbacks

I'm not sure if this actually a benefit, but it had been decided that we did not need to support Internet Explorer 8. As a result, an entire component of complexity of handling fallbacks was gone. Also lost with that was a benefit that icon fonts would have had.

Drawbacks

These drawbacks proved to be inconveniences: not real problems that couldn't be worked with.

No default width/height per icon:

Perhaps our biggest problem was the fact that we could not set a default icon size for the SVGs in the source SVG itself since they were external sprites. I have not found a way to transfer the size set in the external SVG sprite to something the browser would observe when using that sprite.

The default recommendation online is to declare the width and height attributes on the <svg> in the browser, but that can prove inconvenient since they would need to be looked up from the source—especially if the icon is not square. The other option is to declare them in CSS, but that would then override the width and height attributes.

What we did was stay with the width and height attributes. For icons used in .phtml templates, this was quite simple because the SVGs were easy to look up. For icons used in the Magento CMS, we created a widget to render the icon. The width and height still need to be declared, but most of the icons in use there are square. Also, if the icon is square, only the width needs to be declared, and the widget will copy the value to the height.

Summary: no perfect solution.

Use Syntax is verbose:

The entire <use> syntax is quite long and unnatural for content authors that would only be familiar with more basic HTML. In our case, we used a widget to render it in the Magento CMS. As a result, it doesn't take much at all to render an icon.

For the templates, we used a Magento Helper class to make the markup more concise. The helper passes the necessary arguments through to the same template used for the widget.

Summary: after the initial setup, this worked great.

Use of SVGs in CSS pseudo elements:

There are instances where CSS pseudo-elements are simply better than including the icon directly in the DOM. Since we can't (quite) use SVG sprites in CSS, we have to reference the external SVG itself. What we did was create a separate folder for SVGs that were to be used in this way. Gulp would then minify the SVGs and put them into a different folder where they could be used. We had to declare colors directly in the SVG itself as well so it worked well to have them separate anyway. The number of icons used in this way is actually quite small so this hasn't been a problem.

Summary: not an ideal, but still feasible workaround.

System

We use Gulp for building the front end assets. The gulp-svg-sprite allowed us to have fine-grained control over the output of the SVG files.

Process

As I mentioned earlier, there are several different categories of icons used on the site. Each category is a folder of SVG files. It would be like this:

/site
    /checkmark.svg
    /close.svg
    /download.svg
    /plus.svg

When the Gulp task runs, it will create an SVG file named site.svg. Inside of that file are many <symbol> elements that are named based on the files: checkmarkclosedownloadplus. The icons will be displayed with HTML like this:

<svg width="20" height="23" class="icon-checkmark">
    <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/skin/frontend/theme/default/images/dist/site.svg#checkmark"></use>
</svg>

To add an additional icon to the site category is as simple as dropping an SVG into that folder. Gulp watches it and will re-compile the output SVG.

Directory structure

The folder structure would be something like the following. In a single theme, it would look like this, inside the skin/images folder:

/svg
    /site
        /checkmark.svg
        /close.svg
        /download.svg
        /plus.svg
    /objects
        /people.svg
        /gym.svg

Gulp task

Now some code! Let's take a look at the Gulp task that handles the icon system. To start with, we will go through the main task in its (near) entirety. Note that there are a number of config objects above this, but I think the naming should make it somewhat self-explanatory. Also, keep in mind that this is more complicated than it otherwise would be because of the Magento theme folder structure.

gulp.task('svg', function() {
    var themes = themeArray(),
        tasks = themes.map(function(theme) {
            var spriteFolders = getFolders(theme + paths.images.spriteFolders, true);

            return spriteFolders.map(function(sprite) {
                var svgConfig = clone(config.svg);
                svgConfig.mode.symbol.sprite = path.join(theme, paths.images.dist, sprite + '.svg');

                return gulp.src([
                        path.join(theme, paths.images.spriteFolders, sprite, '*.svg'),
                        paths.images.alwaysInclude + sprite + "/*.svg"
                    ]).on('end', () => { gutil.log(`Compiling ${gutil.colors.magenta(sprite + '.svg')}`); })
                    .pipe(svgSprite(svgConfig).on('error', error => { logError('SVG Sprite Error', error);}))
                    .pipe(rename(sprite + '.svg'))
                    .pipe(gulp.dest(path.join(theme, paths.images.dist)));
            });
        });

    return merge(tasks);
});

When the SVG task runs, it loops through an array of paths to the various themes in use on the website. They are returned from the themeArray() variable. Inside the single theme directory, a list of the folders (categories) are obtained.

Here is the getFolder function:

function getFolders(dir) {
    try {
        return fs.readdirSync(dir)
            .filter(file => {
                return fs.statSync(path.join(dir, file)).isDirectory();
            });
    } catch (e) {
        return [];
    }
}

Inside each folder (SVG group), a copy of the base svg-sprite configuration is created and the sprite property is modified to the current group's name. After the configuration is modified, there are some familiar Gulp lines. In gulp.src, the second array item is the global theme's sprites. This allows us to have many of the icons only in the global theme while the child themes override or change if necessary.

The actual compilation happens in a simple-looking call to the gulp-svg-sprite plugin: svgSprite(svgConfig). The sprite file is then named based off of the group folder and saved.

Finally, merge is a call to the merge-stream plugin where all the Gulp tasks in the loop actually get run.

Gulp, SVGs, and bumps

That's how we did the SVG icon system. As more browsers support SVG fragments, I suspect that will quickly become a superior approach. For the time being, these type of solutions keep pushing SVG to the forefront of the web, and we are pleased with the results.

Jesse Maxwell

COO / Senior Developer at SwiftOtter - @bassplayer_7