How to work with SVG icons

Florens Verschelde

March 2018 update: more reliable way to set alternative text, small fixes.

There are many ways to use SVG icons in HTML and CSS, and I haven’t tried them all. This is how we do it in our small front-end team at Kaliop. It works well for our needs, which include:

This article is available in French and in Chinese. Many thanks to the translators!

Preparing your icons

When you get a SVG icon from a designer or from a graphics tool you use (Illustrator, Adobe Assets, Sketch, Inkscape, etc.), it’s tempting to just throw it into your project. Yet I find that reworking it a little bit in your favorite tool to make sure it’s just right can save you some headaches and improve the result.

Simple icon on an artboard in Illustrator (left) and Sketch (right)

Work with a new document or artboard

Create a new document or new artboard in your favorite tool, and copy-paste your icon in the center. It’s a great way to make sure your icon is clean and doesn’t have a ton of hidden paths lying around.

Square is easier

Your icon doesn’t have to be square, but square icons are easier to work with (unless your icon or graphic is really wide or really tall).

Exact dimensions only matter if you want to micromanage pixel fitting (to get the sharpest possible results on low dpi screens). For example if all your icons can fit on a 15 by 15 pixel grid, and are mostly used with those exact dimensions, go ahead and work with 15×15 artboards or documents. When I’m not sure, I like setting stuff on a 20×20 artboard.

Breezy on the sides

Leave a little bit of space near the edges, especially for round shapes. Browsers use anti-aliasing when rendering SVG shapes, but sometimes the extra pixels from the anti-aliasing are rendered outside of the viewBox and they’re cut off.

Screenshot of an round icon in Illustrator, touching the limits of the artboard
We didn’t leave any space around the icon, so there’s a risk that it will be rendered with squarish sides. And if the browser doesn’t render the SVG perfectly, it can get worse.

As a rule of thumb, in a 16px or 20px icon, leave 0.5px or 1px of empty space on each side. Also, remember to export the whole artboard, not the selected paths at the center, or you will lose that white space in the export.

Export to SVG

Learn some SVG

You definitely should learn some SVG basics, and be able to read and understand the structure of simple SVG files. Ideally you should know about these:

I tend to look them up in DevDocs (which simply offers a nicer view of the MDN SVG docs) when I want to know more. You don’t have to do that right now, and you can start your SVG adventure by copy-pasting code, but in time it’s helpful to learn enough to understand the code you’re copy-pasting.

When you export SVG from a design tool, it will often have a little bit or a lot of unnecessary markup, metadata and such. It can also have excessively precise path data (in the d attribute). Try using a tool such as SVGOMG and compare the before and after code to see what gets removed or simplified.

Remove color data

For single-color icons, make sure that:

  1. In your source file, the paths are black (#000000).
  2. In the exported code, there are no fill attributes.

If we have hardcoded fills in the SVG source, we won’t be able to change those colors from our CSS code. So it’s generally best to remove them, at least for single-color icons.

Illustrator doesn’t output fill attributes for path that are fully black (#000000). Sketch does, so you may have to open the exported SVG code and manually remove the fill="#000000" attributes.

Making a SVG sprite

This part has a lot of code, but it’s actually not complex at all. We want to create a SVG document containing <symbol> elements. Each <symbol> must have an id attribute, a viewBox attribute, and will contain the icon’s <path/> elements (or other graphical elements).

I’m calling this SVG document a sprite (in reference to sprites in computer games and CSS), but it may also be called a sprite sheet, or a symbol store.

Here’s a sprite with just one icon:

<svg xmlns="http://www.w3.org/2000/svg">
  <symbol id="cross" viewBox="0 0 20 20">
    <path d="M17.1 5.2l-2.6-2.6-4.6 4.7-4.7-4.7-2.5 2.6 4.7 4.7-4.7 4.7 2.5 2.5 4.7-4.7 4.6 4.7 2.6-2.5-4.7-4.7"/>
  </symbol>
</svg>

Adding an icon to our sprite

Say that Illustrator gave us this code for the icon show above:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
  viewBox="0 0 15 15" style="enable-background:new 0 0 15 15;" xml:space="preserve">
  <path id="ARROW" d="M7.5,0.5c3.9,0,7,3.1,7,7c0,3.9-3.1,7-7,7c-3.9,0-7-3.1-7-7l0,0C0.5,3.6,3.6,0.5,7.5,0.5 C7.5,0.5,7.5,0.5,7.5,0.5L7.5,0.5L7.5,0.5z M6.1,4.7v5.6l4.2-2.8L6.1,4.7z"/>
</svg>

We can simplify it quite a bit (manually or using SVGOMG), only keeping the viewBox attribute and essential data:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
  <path d="M7.5,0.5c3.9,0,7,3.1,7,7c0,3.9-3.1,7-7,7c-3.9,0-7-3.1-7-7l0,0C0.5,3.6,3.6,0.5,7.5,0.5 C7.5,0.5,7.5,0.5,7.5,0.5L7.5,0.5L7.5,0.5z M6.1,4.7v5.6l4.2-2.8L6.1,4.7z"/>
</svg>

Now we can copy-paste it in our sprite. We need to transform the <svg viewBox="…"> element into a <symbol id="…" viewBox="…"> element and insert it manually into our sprite:

<svg xmlns="http://www.w3.org/2000/svg">
  <symbol id="cross" viewBox="0 0 20 20">
    <path d="M17.1 5.2l-2.6-2.6-4.6 4.7-4.7-4.7-2.5 2.6 4.7 4.7-4.7 4.7 2.5 2.5 4.7-4.7 4.6 4.7 2.6-2.5-4.7-4.7"/>
  </symbol>
  <symbol id="play" viewBox="0 0 15 15">
    <path d="M7.5,0.5c3.9,0,7,3.1,7,7c0,3.9-3.1,7-7,7c-3.9,0-7-3.1-7-7l0,0C0.5,3.6,3.6,0.5,7.5,0.5 C7.5,0.5,7.5,0.5,7.5,0.5L7.5,0.5L7.5,0.5z M6.1,4.7v5.6l4.2-2.8L6.1,4.7z"/>
  </symbol>
</svg>

You can do this manually for all your icons, but there are tools that automate the process. We use gulp-svg-symbols (here’s our gulp config, if you’re curious), but there are several graphical and command-line tools that export SVG symbol sprites, including Icomoon.

Pro tip: Keep a folder with your source icons

If you make your sprite manually, I recommend keeping a folder with individual SVG icons:

assets/
  icons/
    cross.svg
    play.svg
    search.svg
    ...
public/
  sprite/
    icons.svg

Then if you need to rebuild the icons.svg or change an individual icon, you have the right sources to work with. Be careful to not let your sprite and your source folder get out of sync.

Of course if you use a build process (with Gulp or Webpack or something else), you can feed it your source folder and let it build the sprite directly.

Important: not everything is an icon

Just because something is SVG, it doesn’t mean it should go inside your SVG sprite. For instance:

As a rule of thumb, if it’s a big illustration that you need to show at 100px by 100px or much bigger, or if it contains dozens of elements, it might not be an “icon”. It would probably bloat your sprite’s size, too.

Adding icons to your pages

The bad news is that in order to use our SVG icons, we have to change our HTML code. No CSS backgrounds, no ::before pseudo-elements. The least verbose HTML we can use is:

<svg>
  <use xlink:href="/path/to/icons.svg#play"></use>
</svg>

Which can look like this:

An unstyled SVG icon

Now we’re going to make this HTML a bit more verbose, in order to make it easier to style and also more accessible. I’ll show a few common patterns first, and then we’ll explain each technical choice.

Pattern A: a purely decorative icon

<svg class="icon" aria-hidden="true" focusable="false">
  <use xlink:href="sprite.svg#symbol-id"></use>
</svg>

Pattern B: link or button with icon and text

<a href="/news">
  <svg class="icon" aria-hidden="true" focusable="false">
    <use xlink:href="sprite.svg#newspaper"></use>
  </svg>
  Latest News
</a>

Pattern C: link or button with icon

This is the same as (B), but we only want to show an icon. We should still keep the text around, so that screen reader users can read it.

<a href="/news" title="Latest News">
  <svg class="icon" aria-hidden="true" focusable="false">
    <use xlink:href="sprite.svg#newspaper"></use>
  </svg>
  <span class="access-label">Latest News</span>
</a>

Pattern D: icon and alt text outside of a link or button

For example, if we had a table column with icons representing “Yes” and “No”:

<td title="Yes">
  <svg class="icon" aria-hidden="true" focusable="false">
    <use xlink:href="sprite.svg#check"></use>
  </svg>
  <span class="access-label">Yes</span>
</td>

What do our different HTML parts do?

HTML pattern What it does
<a title="My label"> This is mostly a usability enhancement for some users: when you’re only showing an icon to represent a feature, it can help to show a tooltip on mouseover.
<span class="access-label">…</span> We’re using a visually hidden <span> to provide accessible text. Other techniques like aria-label or SVG’s <title> element are not that well supported in browsers and screen readers, so this is the most reliable way. We’ll discuss the exact styles to use for our access-label class in the next section.
aria-hidden="true" Since we’re already including accessible text outside of the SVG element, we can tell screen readers to ignore it. This prevents issues where some readers might try to read something for the icon, or read the link or button’s text twice.
focusable="false" This one is for IE and Edge. In those browsers, the SVG element can get keyboard focus. This can result in the surrounding link or button being focused twice, and being read twice.
class="icon" We could style the SVG element directly, but adding a class and reusing that class everywhere lets us define useful default styles for our SVG icons.

Using meaningful and localized labels

Some articles recommend associating a text label with each SVG file in your icon repository (e.g. by putting a <title> element in each <symbol>). This is a mistake, because a visual symbol can have different meanings depending on context.

A “magnifying glass” icon can mean “Show the search form” in one context, and “Submit my search query” in a second context. Your text labels should then be “Show the search form” and “Submit search query”, not “Search” (too generic) or worse “Magnifying glass” (describes the visuals and not the meaning).

On multilingual sites, your text labels should also be localized, including all text labels that are hidden by default for sighted users.

External and inline sprites

Up until now we’ve shown examples for an external sprite. But some older browsers — namely, IE 9 to IE 11 — only support inline references for <use xlink:href="#some-id"/>.

This can be polyfilled with some JavaScript (svg4everybody, svgxuse). Or you can decide to include your sprite in the HTML code of every page.

<body>
  <!-- Hidden icon data -->
  <svg aria-hidden="true" focusable="false" style="display:none">
    <symbol id="icon-play">…</symbol>
    <symbol id="icon-cross">…</symbol>
    <symbol id="icon-search">…</symbol>
  </svg>

  <!-- A visible icon -->
  <button>
    <svg aria-hidden="true" focusable="false">
      <use xlink:href="#icon-play"></use>
    </svg>
    <span class="access-label">Start playback</span>
  </button>
</body>

Each method has its pros and cons.

Inline sprite External sprite
Browser support

Native support in IE9+.

Older Safari/WebKit needs the sprite at the beginning of the page (before any reference).

Native support in Edge 13+, Safari 9+.

IE and older Safari/WebKit need a JS polyfill such as svg4everybody or svgxuse.

Loading an external sprite from a different domain doesn’t work for now (even with CORS, see Chromium bug for instance). This can be polyfilled with JS using svgxuse.

Caching

No caching.

The weight of your sprite (5KB, 15KB, 50KB…) is added to every page.

Sprite is a separate file and can be cached by the browser.

Also it does not bloat your server-side HTTP cache.

Rendering speed

Icons are rendered instantly.

On slow networks, rendering of the page structure and content may be delayed if you have a heavy SVG sprite inserted before your content.

Icons can be shown a bit late because a) they require a separate HTTP request and b) the browser did not prioritize loading this SVG file (using its look-ahead pre-parser).

Updating

When you need to update your sprite, you need to make sure it’s added to every single page. This regenerating all HTML pages if you use a static generator, or invalidating all caches.

Only one public file is changed, so managing updates and server-side caching is a bit easier.

With a CDN

Not impacted.

Warning: browsers will not load SVG referenced with xlink:href on a different domain or subdomain (cross-origin)! If you need to load a cross-origin SVG sprite, you will need to load it with JavaScript and inject it in the page.

I like mixing both methods, building two sprites:

  1. A small one with essential icons (e.g. main icons used in the page header), to be inlined on each page. Target size: 5KB or less.
  2. A bigger one with all the project’s icons, kept as an external file. Target size: 50KB or less.

On bigger projects, we might add more external sprites when some icons can be grouped together and are only used in one part of the site or for a specific feature.

Styling icons with CSS

Now that we have SVG icons and a SVG sprite, and that we learned how to add icons to our HTML, let’s look at the CSS side.

Starting with base styles

Here are the styles for our two utility classes, icon and access-label.

/**
 * Visually hidden accessible label
 * Using styles that do not hide the text in screen readers
 * We use !important because we should not apply other styles to this hidden alternative text
 */
.access-label {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
  white-space: nowrap !important;
}

/**
 * Default icon style
 */
.icon {
  /* Use the current text color as the icon’s fill color. */
  fill: currentColor;
  /* Inherit the text’s size too. Also allows sizing the icon by changing its font-size. */
  width: 1em;
  height: 1em;
  /* The default vertical-align is `baseline`, which leaves a few pixels of space below the icon. Using `center` prevents this. For icons shown alongside text, you may want to use a more precise value, e.g. `vertical-align: -4px` or `vertical-align: -0.15em`. */
  vertical-align: middle;
  /* Paths and strokes that overflow the viewBox can show in IE11. */
  overflow: hidden;
}

With that code, by default your SVG icons should be small and use the parent’s text color. Like this:

First row of icons with default text size and color
Second row of icons with bigger text size and a green text color
Icons with our default style. The only change between the top and bottom row is the font-size and color of the container.

If the icon’s shapes do not inherit the parent’s text color (currentColor), check that you don’t have hardcoded fill attributes in your icon’s code.

Why not style the svg selector directly?

We might have other SVG elements in the page that are not icons. For example, a big SVG map, or an interactive SVG game. Another reason was that older Firefox versions (before Firefox 56) had a bug with the svg selector (it would apply inside the <use> element), and targetting a class avoided this issue.

Feel free to use the svg selector instead of .icon if you think that would work for your use case.

Overriding the base styles

It should be easy to override our base styles in a specific context:

.ShareComponent .icon {
  /* Change the width and height */
  font-size: 40px;
  /* Change the fill color */
  color: purple;
  /* Change the vertical-align if you need more precision
     (sometimes needed for pixel-perfect rendering) */
  vertical-align: -4px;
}

Inherited SVG styles

Many SVG style properties are inherited. For instance when we set the fill CSS property on our containing <svg> element, it trickles down to our <path>, <circle> and other graphic elements.

We can use this technique for other SVG CSS properties as well. For instance the stroke properties:

.icon-goldstar {
  fill: gold;
  stroke: coral;
  stroke-width: 5%;
  stroke-linejoin: round;
}
Star icon with default and custom styling

Most of the time we’re not going to change a lot of stuff: only the fill property for the main color, and sometimes we’ll add or tweak a stroke (kinda like a border).

Two fill colors per icon

There’s a fairly simple technique which allows an icon to have two set of paths with two different fill values (aka two colors).

<symbol id="check" viewBox="0 0 20 20">
  <!-- Will inherit the value of the fill CSS property -->
  <path d="…" />
  <!-- Will inherit the value of the color CSS property -->
  <path fill="currentColor" d="…" />
</symbol>
.icon-bicolor {
  fill: rebeccapurple;
  color: mediumturquoise;
}
Two color icons

Tip: leave some room for strokes

Remember when we said to leave some room around your shapes? It’s especially important if you plan to use strokes.

.icon-strokespace {
  fill: none;
  stroke: currentColor;
  stroke-width: 5%;
}

In SVG, strokes are always painted on both sides of the path (inside and outside). If your path touches the limits of the viewport, half the stroke will be cut off.

In this example, the first icon has no reserved blank space on the sides, and the second icon has a small one (0.5px, for a 15px viewport).

Tip: use percentages for stroke-width

Giving the right size to a stroke can be challenging. Look at these two examples where we set stroke-width:1px on the <svg> element:

What’s happening? The stroke-width property takes a “length” value, but that value is relative to the local coordinates of your icon. In the examples above:

  1. The first icon has a 20px by 20px viewBox. So a 1px stroke is 1/20th of the icon’s size, big but not too big.
  2. The second icon has a 500px by 500px viewBox. So a 1px stroke is 1/500th of the icon’s size, and that really really small.

If all your icons use a similar viewBox, that’s not a problem. But if they can differ wildly, using pixel or unitless values (stroke-width:1) is out. What should we do?

Percentages can be good. Same example, with stroke-width:5%:

Now that’s better. (For square icons, stroke-width:N% will work perfectly, but note that it can behave a bit differently for large or tall SVG elements.)

Advanced topics and tricks

With the previous sections you should have everything you need to know to use SVG icons. This next section adds some more advanced information, with perhaps fewer real-world applications.

Avoiding really big unstyled icons

What happens if your main stylesheet fails to load because the user is on a flaky connection, maybe on a train (this happens to me all the time)? The page might still render without styles.

If you have a decent HTML structure, the page will still be readable, but your icons will end up really big.

Screenshot of unstyled SVG icons
Recent browsers size the <svg> element at 300 by 150 pixels by default. Other browsers might give it a gigantic 100% width!

I recommend putting this in the <head> of your pages:

<style>.icon{width:1em;height:1em}</style>

Short and sweet.

Another approach is to always use width and height attributes on your SVG elements. It works, but if you need to resize this icon in CSS it might be a bit harder.

<svg width="20" height="20" aria-hidden="true" focusable="false">
  …
</svg>

Preloading external sprites

In the “Adding icons to your pages” section we said that icons from an external sprite can show up a bit late, partly because of the browser’s pre-load scanner (or look-ahead pre-parser or whatever) not picking up that <use xlink:href="/path/to/icons.svg#something"></use> means there is an important file to load early.

What can we do about this?

I haven’t actually tested those solutions, generally the “inline + external” compromise gives good enough performance that we don’t have to rely on preloading. But it’s worth investigating.

Selecting individual shapes or paths

We’ve looked at ways to customize fills, strokes etc. for all paths from a <symbol>, and 2 or more colors from different paths. But what if we could select specific paths (using classes maybe) directly in the instance of the <symbol>? Is it possible?

Right now the answer is: yes and no.

  1. If you use external sprites, you can’t select individual paths (or other elements) inside a used <symbol>.
  2. If you inline your sprite, you can select and style elements inside the sprite, but those styles will apply to all instances of the symbols.

So even with an inlined sprite, you could do this:

#my-symbol .style1 {
  /* Styles for one group of paths */
}
#my-symbol .style2 {
  /* Styles for another */
}

But you can’t do this:

.MyComponent-button .icon .style1 {
  /* For 1 group of paths for this icon in this context */
}
.MyComponent-button .icon .style2 {
  /* For another group */
}

In the future, there might be a standard way that allows selecting through a Shadow DOM, but that’s not sure at all (there used to be the /deep/ combinator, but it was removed).

More than two colors with CSS Custom Properties

So we can easily change colors of our SVG icons from CSS, for single-color icons (easy) and two-color icons (takes some preparation). Is there a way we could have multicolor icons with more than two customizable colors?

We might be able to do that with CSS Custom Properties (aka CSS Variables). This requires a lot of preparation on the SVG side:

<symbol id="iconic-aperture" viewBox="0 0 128 128">
  <path fill="var(--icon-color1)" d="…" />
  <path fill="var(--icon-color2)" d="…" />
  <path fill="var(--icon-color3)" d="…" />
  <path fill="var(--icon-color4)" d="…" />
  <path fill="var(--icon-color5)" d="…" />
  <path fill="var(--icon-color6)" d="…" />
</symbol>

For this demo I stole an icon from the excellent Iconic, which offers responsive, multicolor SVG icons (powered by CSS and some JS, as I understand). I tried to mimick their own multicolor example for this icon, I hope they don’t mind.

One symbol using 6 different CSS custom properties.
See in Firefox, Chrome, or Safari 9.1+

It works fairly well in supported browsers. There’s only one icon: the first instance doesn’t declare the expected variables, so it falls back to currentColor; the next two instances each declare a set of variable values.

How are percentage stroke-width computed?

What does the percentage correspond to in stroke-width:N%? Is it the width, the height of the icon? Turns out it’s relative to the diagonal, but with a funky formula (spec) (diagonal length divided by the square root of 2, which is close to 1.4).

What does it mean? Well for square icons the result of that formula is the side of the square. So 1% means “one percent of the width or one percent of the height”. Nice and simple.

For wider or taller icons, though, the result can change a bit:

In the second icon (aspect ratio of 2:1, shown with the same height and twice as large), the stroke-width:5% gives us a stroke that is roughly: 7.91% of the height and 3.95% of the width.

All things considered, I still recommend using percentage values for stroke-width. If you stick to square or sharish icon, you can use percent values with the understanding that they mean, roughly, “percentage of the width of the icon”.

Using gradient fills

We can’t use CSS linear-gradient(…) because this sadly cannot apply to the SVG fill property. If we want gradient fills, we will need to use SVG gradients.