For good reason, icon fonts and drawing with CSS pseudo-elements are out, and SVGs are in! But unfortunately, SVGs also have their fair share of (different) issues to contend with.

SVGs have been championed as providing the best developer and end-user experience for icons because they can be styled, manipulated and transformed however we wish, are resolution-independent (so look beautiful on your fancy 8k screen) and their contents can be made somewhat accessible.

The two methods to use SVG for icons are:

  1. Inline SVG; and
  2. SVG icon system with <symbol> and <use>

Inline SVG

This method involves adding whole <svg /> tags with all of your groups, paths and whatever else directly in the page markup where you want it to be rendered. Ain't no one got time for this…

  <button>
  <svg xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 64 64">
    <path d="M24 46c0-9.452 5.992-17.53 14.377-20.637 1.017-2.284 1.623-4.823 1.623-7.363 0-9.941 0-18-12-18s-12 8.059-12 18c0 6.191 3.594 12.382 8 14.864v3.299c-13.569 1.109-24 7.776-24 15.837h34.832c-0.541-1.908-0.832-3.921-0.832-6z"></path>
    <path d="M46 28c-9.941 0-18 8.059-18 18s8.059 18 18 18c9.941 0 18-8.059 18-18s-8.059-18-18-18zM56 48h-8v8h-4v-8h-8v-4h8v-8h4v8h8v4z"></path>
  </svg>
  Add User
</button>

You could write some kind of server-side include to parse a custom tag and re-write it with the <svg> markup, but then you're just hiding the shame - putting lipstick on a pig, if you will.

And anyway, the Add User SVG above is 700 bytes. Multiply that by however many times that icon will be visible in your application, and you've drastically increased your page weight (although gzipping your HTML will negate some of the repetition).

Other alternatives suggest linking to an external SVG with an <img> tag, and then have JavaScript replace them with the actual <svg> contents. While this improves the ease of authoring, you've now added multiple, additional requests to your page load (one for each SVG), and then have to make potentially expensive DOM modifications for all existing and future SVGs. No deal!

So what are inline SVGs good for? Customisation! As the <svg> markup is in the page, you have complete access to it on the front-end, so you can colour, hide, transition and pretty much do whatever else you might want to do to any element within the <svg>

Want that + to turn to a - under certain circumstances? Hide the vertical bar with CSS' display: none!

OK, moving on…

SVG icon system with <symbol> and <use>

This method is a little more involved as you need to create a sprite of all your SVGs. You could do it by hand (if you're mad), or if you use grunt in your build process, grunt-svgstore, is your best friend. It takes a folder (or whichever SVGs you specify) and creates a sprite from them. There is also a version available for gulp, which, while not quite as full-featured, does the job.

Now that you have your sprite, you need to include it before any <use> references, so generally at the top of your <body>. There are a couple of options to do this:

  1. Drop the entire sprite <svg> markup in to your page raw, or with a server-side include.

    Note that in my experience, SVGs are generally bigger than their icon font counterparts, so this could easily reach up to 100kb of inline, render-blocking markup which will result in a blank white screen while it's parsed.

    When we introduced an SVG icon system at Campaign Monitor, we developed a way to specify exactly which icons to include per page to reduce the initial payload size. However, if we added a new component, we would then have to update the SVGs to include, too; it was a trade-off.

  2. AJAX the sprite and add it to the DOM.

    As long as your interface doesn't rely on the icons to be usable (hint: it shouldn't without a reliable fallback), then asynchronously loading the sprite with something like this is a pretty good option.

    Pros

    • No mass of render-blocking <svg> markup to increase the page size.
    • The sprite will be cached for subsequent page loads (so you will also need to create a cache-busting mechanism).
    • The SVG can be gzipped independently.
    • You can have separately styled states for "not yet loaded", "loaded" and "errored" by applying appropriate classes with the AJAX response.

    Cons

    • It's reliant on JavaScript to work.
    • There will be a Flash Of No Icons (FONI) until the SVG is fetched and added to the DOM.

Now that you've got your SVG icon definitions in your markup, you need to reference them where needed, like so:

  <button>
  <svg xmlns:xlink="http://www.w3.org/1999/xlink">
    <use xlink:href="#add-user" />
  </svg>
  Add User
</button>

That's much neater than the inline method! So, all is great, right? Not quite…

As the SVG element is actually a <use> clone of the original definition, we don't have access to its paths to fully style with CSS, which is one of the main advantages of SVG in the first place! Aarrgghh!

Firefox is currently the only browser which allows us to style the paths with standard CSS selectors:

  button:hover,
button:focus path {
  &:first-child {
    display: none;
  }

  &:last-child {
    transform: scale(1.1);
  }
}

Curse you, other browsers!

This basically leaves us with only being able to change an SVG path's colour using fill. Luckily, with a creative use of currentColor, we can increase our customisable colours per SVG from 1 to 2. But what about our earlier example of converting a + to a - if we can't use display: none? Well, you could say that something is invisible when it's absent of colour — and transparent is a colour!

Let's use the following SVG which draws a + symbol with vertical and horizontal paths.

  <svg xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol viewBox="0 0 64 64" id="show-hide">
    <path d="M 27 0 L 37 0 L 37 64 L 27 64 L 27 0 Z"/>
    <path d="M 64 27 L 64 37 L 0 37 L 0 27 L 64 27 Z"/>
  </symbol>
</svg>

<button>
  <svg xmlns:xlink="http://www.w3.org/1999/xlink">
    <use xlink:href="#show-hide" />
  </svg>
  Show/Hide
</button>

We want the button to have red text and a green + by default, and on hover, change the icon's colour to blue and hide the vertical path. How could we achieve that?

  button {
    color: red;
}

button svg {
  fill: green;
}

button:hover svg,
button:focus svg {
  fill: blue;
}

This turns it all blue, but with only 1 fill property available, how can we hide the vertical path? Let's add fill="currentColor" to the first path in the sprite definition:

  <svg xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol viewBox="0 0 64 64" id="show-hide">
    <path fill="currentColor" d="M 27 0 L 37 0 L 37 64 L 27 64 L 27 0 Z"/>
    <path d="M 64 27 L 64 37 L 0 37 L 0 27 L 64 27 Z"/>
  </symbol>
</svg>

Hmmmm the vertical path is now red by default, as its fill is inheriting the button's currentColor.

OK, let's tweak the CSS to style the vertical bar back to green with color and remove it on hover:

  button svg {
  color: green;
  fill: green;
}

button:hover svg,
button:focus svg {
  color: transparent;
  fill: blue;
}

And voila! The icon changes to - on hover!

OK, so it's a bit of a hack, but it works! You can also use rgba() to colour something with varying opacity.

Sadly, for manipulating SVG paths (transitioning a hamburger icon to an arrow, for example), it will have to work in Firefox only, or be achieved through inlining that SVG. In the future, we'll hopefully be able to style the cloned SVG via the Shadow DOM so we get all of the benefits of SVG in one, neat package.

So there we go. No icon system is perfect, but an AJAX'd SVG sprite icon system is the best we can do for now. As with most things in web development, life is a series of compromises anyway.