Tip of the week #003: Icons with inline SVG

Adding icons to a website can be done in many different ways, but my favorite approach is to use SVGs inline in my HTML. Here's how.

The struggles with icons

Implementing icons have always been somewhat a pain in the ass for me. To elaborate I have to walk you through the pros and cons of four techniques on how to add icons, and the problems that arise with the use of each.

Using <img>

This is the approach most people use, to add .png or .svg icons using <img> tags:

Language: html
<button>
  <img src="icon-save-white.png" />
  <span>Save</span>
</button>
Language: css
button img {
  width: 24px;
  height: 24px;
}

There's nothing inherintly wrong with this approach, but it could lead to some headache if you need to swap the icon on different states, like when hovering the button.

Language: html
<button>
  <img src="icon-save-white.png" />
  <img src="icon-save-blue.png" />
  <span>Save</span>
</button>
Language: css
button img:nth-child(2) {
  display: none;
}

button:hover img:nth-child(1) {
  display: none;
}

button:hover img:nth-child(2) {
  display: inline-block;
}

You would have to hide/show the images when hovering. The code for this gives me the "ick" (using the phrase "gives me the ick" also gives me the "ick", by the way).

Using background-image

One solution would be to use background-images instead:

Language: html
<button>
  <span class="button-icon"></span>
  <span>Save</span>
</button>
Language: css
.button-icon {
  background-image: url('icon-save-white.png');
  background-size: 24px;
  background-repeat: no-repeat;
}

button:hover .button-icon {
  background-image: url('icon-save-blue.png');
}

This is cleaner, but it still poses some problems. What if I want to animate the icon? There's no way to create a transition between the color white in the first image and the color blue in the second image.

Using an icon font

An alternative to images and background-image is to use an icon font. With an icon font the icons are presented as glyphs, just as normal letters in text. Then you could change colors using css with no problems.

Language: html
<button>
  <i class="icon icon-save"></i>
  <span>Save</span>
</button>
Language: css
.icon {
  font-family: "MyIconFont";
  font-size: 24px;
  color: currentColor;
  transition: color .15s ease;
}

button {
  color: white;
}

button:hover {
  color: blue;
}

However, I haven't used them for several years, as they come with some drawbacks, the main one being that the icons will disappear if users use plugins to change the font on the website. Some users do this to improve the readability of the website, for example if they struggle with dyslexia. In those cases it's fine that they use tools to replace your shiny "Futura" or "Satoshi" font with something more readable, but this will also affect the icons fonts, most likely making all icons show up as a � on the site.

Using a sprite

My least favorite way to do icons is to add all icons to a sprite file and then move the sprite around using background-position to present the correct icon. Like moving the planchette around on a ouija board, basically. If you haven't heard about sprites before this article from 2009 explains it well.

An example of a sprite file. Here all the icons are generated into one file, lining them up in a grid-like pattern, making it possible to show one of the icons at a time using background-position. Credit:http://acronetwork.org
An example of a sprite file. Here all the icons are generated into one file, lining them up in a grid-like pattern, making it possible to show one of the icons at a time using background-position. Credit:http://acronetwork.org

I don't like this approach because it's a lot of work. You have to toss all the icons into a sprite generator like Sprite Cow so that you get a nicely generated stylesheet. This way you don't need to add the background-positions manually. For any new icon you add you have to generate the sprite again, so that positions are updated. Also, you have to add the icons in all the sizes and all the color variants they're gonna be in, as you can't alter the size with CSS later, or change the colors.

It's just too much overhead.


My favorite approach

One option is to — as stated in this article's title — use SVGs directly in your HTML:

Language: html
<button>
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 5V19H19V7.82843L16.1716 5H5ZM4 3H17L20.7071 6.70711C20.8946 6.89464 21 7.149 21 7.41421V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM12 18C10.3431 18 9 16.6569 9 15C9 13.3431 10.3431 12 12 12C13.6569 12 15 13.3431 15 15C15 16.6569 13.6569 18 12 18ZM6 6H15V10H6V6Z" fill="black"></path></svg>
  <span>Save</span>
</button>

No need to sugarcoat it: this markup looks really ugly 💩. It's bloated, hard to read (because SVGs are) and it's difficult to see what the SVG looks like (spoiler: it's a floppy disk icon copied from Remix Icons).

However, it gives me the ability to:

  • target the SVG in my CSS
  • change size and color using CSS
  • animate or transform the icon using CSS

Which is great. But there's still some things we need to take into account. If you've not used SVGs inline in your code like this before it's worth noting that there are some pitfalls. Let's examine the code:

  • the attributes width="24" height="24" sets the icon to be 24x24 pixels
  • viewBox="0 0 24 24" sets the aspect ratio to be 24:24, or 1:1 (meaning it's a square)
  • the fill attribute on the <path> sets the color of the icon to black

These attributes builds the foundation for how we can go about and style the icons, meaning I can override them with something like this:

Language: css
/* If I want the icon bigger I can override the width/height attributes */
button svg {
  width: 32px; 
  height: 32px;
}

/* To change the color I need to target the <path> and override the fill attribute */
button svg path {
  fill: blue;
}

This too feels a bit wonky, don't you agree? Therefore I'd like to make some changes to the SVG markup:

  • Remove the height attribute and change width to width="1em"
  • Change fill="black" to fill="currentColor" on the <path> tag
  • Add a class to the <svg> (for simpler targeting in CSS)
Language: html
<button>
  <svg xmlns="http://www.w3.org/2000/svg" class="button-icon" width="1em" viewBox="0 0 24 24"><path d="M5 5V19H19V7.82843L16.1716 5H5ZM4 3H17L20.7071 6.70711C20.8946 6.89464 21 7.149 21 7.41421V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM12 18C10.3431 18 9 16.6569 9 15C9 13.3431 10.3431 12 12 12C13.6569 12 15 13.3431 15 15C15 16.6569 13.6569 18 12 18ZM6 6H15V10H6V6Z" fill="currentColor"></path>
  </svg>
  <span>Save</span>
</button>
Language: css
.button-icon {
  font-size: 32px;
  color: blue;
}

This way I can write simpler CSS, adding color to the icon using color (like you would do with text) and change the size simply by setting font-size (like you also would do with text), due to the width of 1em that we wrote. And why did we remove the height attribute? Well, the viewBox="0 0 24 24" handles the aspect-ratio of the SVG, so if we remove either height or width, and only sets one of them, the SVG will scale accordingly when one of the values changes. In this case the height will change automatically if the width is altered.

Ways to simplify this

It feels weird to write this much markup just to add an icon in our design. Like many people's gripes with long Tailwind classes, a lot of SVG code in the markup clutters up our DOM. Luckily, in modern web development we rarely work with large HTML files at a time. Most likely our code is split into multiple chunks and reused all over the place. We can do the same with these icons, so that you almost never have to "see" the cluttered markup. Like in JSX:

Language: IconSave.jsx
const IconSave = () => {
  return (
      <svg xmlns="http://www.w3.org/2000/svg" class="button-icon" width="1em" viewBox="0 0 24 24"><path d="M5 5V19H19V7.82843L16.1716 5H5ZM4 3H17L20.7071 6.70711C20.8946 6.89464 21 7.149 21 7.41421V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM12 18C10.3431 18 9 16.6569 9 15C9 13.3431 10.3431 12 12 12C13.6569 12 15 13.3431 15 15C15 16.6569 13.6569 18 12 18ZM6 6H15V10H6V6Z" fill="currentColor"></path></svg>
  );
};

export default IconSave;
Language: Button.jsx
import IconSave from './Icons/IconSave';

const Button = () => {
  return (
    <button>
      <IconSave />
      <span>Save</span>
    </button>
  );
};

export default Button;

There are many ways to do this, just find the solution that works best for you. On this website I use liquid, so my templates look like this:

Language: icons/save.liquid
<svg xmlns="http://www.w3.org/2000/svg" class="button-icon" width="1em" viewBox="0 0 24 24"><path d="M5 5V19H19V7.82843L16.1716 5H5ZM4 3H17L20.7071 6.70711C20.8946 6.89464 21 7.149 21 7.41421V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM12 18C10.3431 18 9 16.6569 9 15C9 13.3431 10.3431 12 12 12C13.6569 12 15 13.3431 15 15C15 16.6569 13.6569 18 12 18ZM6 6H15V10H6V6Z" fill="currentColor"></path></svg>
Language: button.liquid
<button>
  {% include "icons/{{ icon }}" %}
  <span>{{ text }}</span>
</button>
Language: some-layout.liquid
<div class="toolbar">
  {% include "button" 
    text: "Save" 
    icon: "save" 
  %}
</div>

Either way, its main strengths lies in the ability to style the icons the way you want using CSS 👊


Bonus

A quick Codepen demo of an inline SVG icon:

See the Pen Untitled by Håvard Brynjulfsen (@havardob) on CodePen.