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:
- Content and communication websites, often based on big CMSes, rather than full-JS web apps.
- Icons which are often simple, single-color icons (each potentially used in several different colors depending on context and user interactions). Two-color icons and gradient fills are possible too, but require a bit more work.
- IE11 support.
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 design tool (Illustrator, Sketch, Figma, 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.
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.
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
- In Illustrator I just use “Save As” and pick “SVG” for the format. (It might be better to use “Export as…” and pick SVG for an optimized result.)
- In Sketch you can select an artboard, click “Make Exportable” on the bottom right, and pick “SVG” for the format.
- In Inkscape you can “Save As” and pick “Optimized 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:
- Grouping elements:
<svg>
,<symbol>
,<g>
. - Visual elements:
<path>
,<rect>
,<circle>
,<line>
. - Styling attributes:
fill
,stroke
,stroke-width
.
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:
- In your source file, the paths are black (
#000000
). - 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:
-
Illustrations that you don’t need to style: keep as a single SVG file, and include it in your page with
<img src="url/to/illustration.svg" alt="…">
. -
Illustrations that you want to animate: consider inlining the full SVG code in your HTML page, so you can select and style specific groups or paths or shapes, animate some parts, etc.
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="icons.svg#play"></use>
</svg>
Which can look like this:
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">
<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">
<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">
<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">
<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. |
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" 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">
<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 |
I like mixing both methods, building two sprites:
- 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.
- 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:
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;
}
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;
}
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.
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:
- 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. - 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.
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">
…
</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="icons.svg#something"></use>
means there is an important file to load early.
What can we do about this?
-
Standard and future solution: add
<link rel="preload" href="/path/to/icons.svg" as="image">
in the<head>
of the page (details on preload, support coming to a Chrome near you and hopefully in more browsers in the future). -
Old school solution: add
<img style="display:none" alt="" src="/path/to/icons.svg">
at the start of the<body>
.
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.
- If you use external sprites, you can’t select individual paths (or other elements) inside a used
<symbol>
. - 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.
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:
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.