DEV Community

Cover image for Implementing Icons

Implementing Icons

madsstoumann profile image Mads Stoumann Updated on ・14 min read

Icons have played an important role since the birth of the Graphical User Interface (GUI). They can either be informative — like icons next to form fields, providing additional visual clues — or actionable, like the infamous “hamburger”-icon, triggering a mobile navigation.

User experience and accessibility

The Nielsen Norman Group summarizes the use of icons like this:
A user’s understanding of an icon is based on previous experience. Due to the absence of a standard usage for most icons, text labels are necessary to communicate the meaning and reduce ambiguity.

Designers will most likely disagree with this, since they often use icons to save space, thus ditching the text labels, and relying on the user recognizing the [meaning of the] icon.
So, designers — if possible — please use icons in conjunction with a descriptive text that's crisp and clear.

An icon should — if it's actionable — always be wrapped in either a <button> , <label> , <details> or an <a>nchor. The wrapping element should contain a textual explanation of what is does, either as visual text, or using the aria-label-attribute. The icon itself should have the aria-hidden-attribute set to true.

As you typically add eventListeners to the wrapping-element, set pointer-events to none for the icon in CSS.

If an icon is informative, add either an aria-label-attribute, or use the <title>-tag in an <svg>.

For longer descriptions, <svg> also has a <desc>-tag

Implementations: SVG

There are many ways of implementing icons.

Nowadays, avoid bitmap formats like PNG or JPG, since these are not crisp and scalable.

Instead, use Scalable Vector Graphics: <svg>.

An <svg> can either be:
- A physical file, you can reference as:

  • The src of an <img>-tag in HTML
  • The xlink:href-attribute of a <use>-tag in HTML
  • The url of a background-image in CSS
  • The url of a mask-image in CSS
  • The data-attribute of an <object>-tag (don't)
  • The src-attribute of an <iframe>-tag (don't)

- Inline markup, that you can use:

  • Directly in HTML
  • As an encoded string in CSS, which can then be used as:
    • background-image
    • content in a pseudo-element
    • mask-image

Note: Your <svg>-files should be as neutral as possible, so they can be used in multiple scenarios.

Remove attributes such as stroke and fill — these will be applied through CSS.
Let's look at the various options.

The least flexible: In an <img> tag

You should already know this:

<img src="../assets/svg/check.svg" alt="Check" class="c-ico" />
Enter fullscreen mode Exit fullscreen mode

This works fine — you can control the size from CSS, but that's about it.

fill and stroke will have to be hardcoded in the svg-file — not very flexible.

The most flexible: inline markup

If you want full control, the best way to add an <svg>-icon, is to inline it in your HTML-markup, like this:

<svg viewBox="0 0 16 16" class="c-ico">
  <path d="M14 2.5l-8.5 8.5-3.5-3.5-1.5 1.5 5 5 10-10z"></path>
Enter fullscreen mode Exit fullscreen mode

You can style it in CSS, like in this example:

.c-ico {
  fill: tomato;
  height: 1em;
  width: 1em;
Enter fullscreen mode Exit fullscreen mode

— But you can also add CSS classes to individual SVG elements:

<path d="..." class="c-ico--custom" />
Enter fullscreen mode Exit fullscreen mode

— And you can use all the usual CSS-tricks:

.c-ico:hover {
  fill: darkred;
  transform: scale(2);
Enter fullscreen mode Exit fullscreen mode

Still flexible: Spritemap using <use> and <symbol>

If you're going to use the icon in many places though, a more practical solution is a spritemap, using <use>- and <symbol>-tags.
You don't have full control, as when using inline, but it's good enough for most icon-use-cases.
You basically move all your <svg>-icons to one central spritemap.svg-file, and link to them, using <use>.

Change the <svg>-tag to <symbol>, add an id-attribute with a unique value, and place it in the <defs>-section as in the example below:

<svg aria-hidden="true"
  style="position: absolute; width: 0; height: 0; overflow: hidden"
    <symbol id="icon-check" viewBox="0 0 16 16">
      <path d="M14 2.5l-8.5 8.5-3.5-3.5-1.5 1.5 5 5 10-10z"></path>
    <symbol id="icon-search" viewBox="0 0 16 16">
      ... etc.
Enter fullscreen mode Exit fullscreen mode

Then, to use it:

<svg class="c-ico">
  <use xlink:href="../assets/svg/spritemap.svg#icon-check" />
Enter fullscreen mode Exit fullscreen mode

Where xlink:href is the path and name of the SVG-file, and the #fragment identifier is the id of the <symbol> you want to use.

As an encoded string in CSS

It is also possible to store an <svg> directly as an encoded string in CSS.

You might have seen examples of an <svg>, encoded with base64:

background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZG...
Enter fullscreen mode Exit fullscreen mode

It comes at a price, though:
It takes 4 characters per 3 bytes of data, plus potentially a bit of padding at the end.

A better and more readable way, is to utf-8-encode it, allowing you to basically “dump” a regular <svg> with line-breaks and spaces removed.

Since utf-8 is the default encoding, it's not even necessary to specify it:

background-image: url('data:image/svg+xml,<svg xmlns="" viewBox="0 0 16 16"><path d="M14 2.5l-8.5 8.5-3.5-3.5-1.5 1.5 5 5 10-10z"></path></svg>');
Enter fullscreen mode Exit fullscreen mode

If you want it more readable, escape each line with a backslash: \
The disadvantage of storing the encoded <svg> directly as a background-image, mask-image or content-property is duplication. You need to “dump” the <svg> for each use. A much smarter way is to store all your most used icons as CSS Custom Properties:

--ico-check: url('data:image/svg+xml,<svg xmlns="" viewBox="0 0 16 16"><path d="M14 2.5l-8.5 8.5-3.5-3.5-1.5 1.5 5 5 10-10z"></path></svg>');
Enter fullscreen mode Exit fullscreen mode

This way, you can re-use the <svg>-data in as many places as you want:

.c-ico--check {
  background-image: var(--ico-check);
.c-ico--checkmask {
  mask-image: var(--ico-check);
.selector::before {
  content: var(--ico-check);
Enter fullscreen mode Exit fullscreen mode

You can also dynamically change an icon by using this method with background-image.

Consider this example:

.c-ico {
  /* excerpt */
  --ico: var(--ico-empty);
  background-image: var(--ico);
Enter fullscreen mode Exit fullscreen mode

It creates a background-image, which by default is set to an empty <svg>.

Change the icon with a modifier-class:

.c-ico--check {
  --ico: var(--ico-check);
Enter fullscreen mode Exit fullscreen mode

No new CSS required — just an update of a variable.

Coloring an <svg>-icon

All of the <svg> presentation attributes can be used as CSS properties, so if you're in-lining your <svg>, you have all presentation options either as attributes in the markup, or as properties in CSS.

Just remember, that background-color is called fill in <svg>!

.custom-svg {
  fill: red;
  stroke: green;
  stroke-width: 3px;
Enter fullscreen mode Exit fullscreen mode

Note: In <svg>, the value of the stroke-width-attribute is relative to the viewBox.

In CSS, use a unit, like px or em etc.


You can style attributes like stroke and fill from CSS, but if you're using a sprite-map, all the <svg>-icons (or: the elements within them, like path or line) will have the same styling.

Maybe that's what you want, but there's one extra option:

currentColor is a reserved CSS-keyword (value), inheriting the current color (following the cascade). If you have an icon with more than one element and want some of the elements to have a different stroke or fill, do this:

<line x1="25" y1="25" x2="75" y2="75" />
<line x1="75" y1="25" x2="25" y2="75" stroke="currentColor />
Enter fullscreen mode Exit fullscreen mode

And in the CSS:

.c-ico {
  color: red;
  stroke: tomato;
Enter fullscreen mode Exit fullscreen mode

The first line-element will use the stroke-property, the second one will use the color-property.

Coloring a background-image.

It is also possible to color an <svg> as a background-image — but it ain't pretty!

You need to set an initial fill-value in either the physical <svg>-file or in the CSS Custom property — and you need to set it to one of the allowed values for: white (#FFF, white etc).

In hsl, the value for “white” is: hsl(0, 0%, 100%), so that's the starting point.
You could also use “black” as your starting point, but for these examples it's “white”.
Now, you can use a CSS filter to change the hue, saturation or lightness from the starting point of hsl(0, 0%, 100%), for example:

.c-ico--pink {
  filter: brightness(0.5) sepia(1) hue-rotate(-70deg) saturate(5);
.c-ico--blue {
  filter: brightness(0.5) sepia(1) hue-rotate(140deg) saturate(6);
Enter fullscreen mode Exit fullscreen mode

Coloring a mask-image

It's much easier to color a mask-image — and if you store your icons as CSS Custom Properties as mentioned above, you have a very flexible combination.
Place an <svg> as the url() of a mask-image-property on an element that also has a background-color.

The <svg> will be “masked” with that color:

.c-ico {
  /* excerpt */
  --ico: var(--ico-empty);
  --ico-bgc: currentColor;
  --ico-h: 1em;
  --ico-w: 1em;
  background-color: var(--ico-bgc);
  height: var(--ico-h);
  mask: no-repeat center/var(--ico-w) var(--ico);
  width: var(--ico-w);
Enter fullscreen mode Exit fullscreen mode

It's super-easy to change the icon and it's color with a modifier-class:

.c-ico--check {
  --ico: var(--ico-check);
  --ico-bgc: tomato;
Enter fullscreen mode Exit fullscreen mode

As the icon here has it's own element (<i> in this example):

<i class="c-ico c-ico--check" aria-hidden="true"></i>
Enter fullscreen mode Exit fullscreen mode

— all the usual CSS-tricks can be applied as well: transform, for example.

Converting existing SVGs to url()

I’ve created a tool, that can convert your existing SVG icons to a CSS url(), so they can be used with background-image or mask-image:

You just drag and drop an SVG-file to the preview-area. If an icon looks weird, try another import option. By default, the option “deep: remove as much” is selected. This option will try to remove as much “clutter” from the SVG as possible (unnecessary tags, comments etc.).

Usage Examples

In the examples below, I've visualized why inline svg, <use> or mask-image are my preferred methods when implementing <svg>-icons (the “checkmark” in the examples).
If you change the text-color, as in the “dark mode” -example, the <svg>'s used as <img> or background-image will not change color.

Light Mode

Dark Mode

Security and <svg>

[...] Because of this ability to contain JavaScript, they are a perfect attack vector for Cross-Site Scripting on sites which allow either arbitrary file uploads or limit the file types to images and accept SVGs.

The quote above is from a blog-post entitled Protecting against XSS in SVG, outlining the security issues related to using <svg>.

<svg> is <xml>, and thus it can contain a regular <script>-block.

If you have an Icon System that automatically in-lines <svg>'s in your final markup, thread carefully, if content-editors are allowed to upload <svg>'s directly.


<svg viewBox="0 0 100 100" class="c-ico--stroke">
  <path d="M25,25 L75,75 M75,25 L25,75"></path>
  <script>console.log('Evil script running...');</script>
Enter fullscreen mode Exit fullscreen mode

When in-lined, the script will run, check the console.
In newer browsers, scripts will not run if the <svg> is used from either <img>-tags or via <use>.

Optimizing <svg>: An example

When you receive an <svg> from a designer, it usually contains way too much unnecessary clutter.

Here's a real-life example of a simple "close icon", I received from a designer:

And it's markup:

<svg xmlns="" xmlns:xlink="" width="12px" height="12px" viewBox="0 0 12 12" version="1.1">
    <!-- Generator: sketchtool 53.2 (72643) - -->
    <desc>Created with sketchtool.</desc>
    <g id="Style" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="Icons" transform="translate(-521.000000, -281.000000)" fill="#6C6F70">
            <g id="Stacked-Group" transform="translate(248.000000, 273.000000)">
                <path d="M280.481608,14.0014021 L284.709147,9.77838915 C285.099578,9.36728524 285.099578,8.72103629 284.693145,8.29553574 C284.278712,7.90042809 283.614659,7.90042809 283.216227,8.30193425 L278.999889,12.5201483 L274.783551,8.30193425 C274.386719,7.90202772 273.722666,7.89722884 273.290631,8.30993238 C272.9018,8.72263591 272.9018,9.36728524 273.298632,9.78638728 L277.51817,14.0014021 L273.298632,18.2228154 C272.9018,18.6211223 272.9018,19.2929653 273.298632,19.6928718 C273.500248,19.8928251 273.756268,20 274.041091,20 C274.348316,20 274.609137,19.8880262 274.783551,19.6928718 L278.999889,15.4810562 L283.203426,19.6784752 C283.393841,19.8880262 283.654662,20 283.958686,20 C284.245109,20 284.50113,19.8928251 284.701146,19.6928718 C285.099578,19.2945649 285.099578,18.6195227 284.701146,18.2212158 L280.481608,14.0014021 Z" id="close"/>
Enter fullscreen mode Exit fullscreen mode

By using Jake Archibald's SVGOMG-tool, a lot of the unnecessary bits can be cut off:

<svg xmlns="" width="12" height="12">
  <path fill="#6C6F70" fill-rule="evenodd" d="M7.482 6.001l4.227-4.223a1.07 1.07 0 0 0-.016-1.482 1.062 1.062 0 0 0-1.477.006L6 4.52 1.784.302C1.387-.098.723-.103.29.31a1.072 1.072 0 0 0 .008 1.476l4.22 4.215-4.22 4.222a1.056 1.056 0 0 0 0 1.47c.201.2.457.307.742.307.307 0 .568-.112.743-.307L6 7.48l4.203 4.197a.99.99 0 0 0 .756.322c.286 0 .542-.107.742-.307a1.055 1.055 0 0 0 0-1.472l-4.22-4.22z"/>
Enter fullscreen mode Exit fullscreen mode

The file went from 1571 bytes to 473 bytes.

But, for such a simple icon, we can also handcode it with two line elements:

<svg xmlns="" viewBox="0 0 100 100">
  <line x1="25" y1="25" x2="75" y2="75" stroke="#6C6F70" />
  <line x1="75" y1="25" x2="25" y2="75" stroke="#6C6F70" />
Enter fullscreen mode Exit fullscreen mode

Or, using path:

<svg xmlns="" viewBox="0 0 100 100">
  <path d="M25,25 L75,75 M75,25 L25,75" stroke="#6C6F70" />
Enter fullscreen mode Exit fullscreen mode

Notice, that I changed width and height to viewBox, which creates a "real coordinate system" and will allow us to easier control responsiveness. Instead of fill, I used stroke.

If the <svg> is going to be used inline or in an sprite-map using symbol , we can also remove the xmlns-attribute and style-declarations (stroke, fill etc.):

<svg viewBox="0 0 100 100">
  <path d="M25,25 L75,75 M75,25 L25,75" />
Enter fullscreen mode Exit fullscreen mode

That's 77 bytes, approx. 5% of the original file-size.
To optimize <svg>'s automatically during a build process, read on.


This webpack-plugin generates a single SVG spritemap containing multiple elements from all .svg files in a directory. In addition, it can optimize the output and can also generate a stylesheet containing the sprites to be used directly from CSS.

Download SvgSpritemapWebpackPlugin if you're looking for an easy way to convert a folder of .svg-files to a single spritemap. While the current version cannot generate CSS Custom Properties from the .svg-files, there are .scss and .less mixin/variables that will let you use them from CSS.

.example {
  /* Using the included sprite() mixin */
  @include sprite('check');
  /* Using the SVG from the map directly */
  background-image: url(map-get($sprites, 'check'));
Enter fullscreen mode Exit fullscreen mode

SvgSpritemapWebpackPlugin can also run svgo and svg4everybody automatically, check out the official documentation.

Polyfill for using <use>

IE 11 doesn't support <use>, so you'll need a poly-fill (if you still need to support IE11!).
svg4everybody is a popular poly-fill for using <use> in Internet Explorer, and is highly recommended.

Below is a simple example of conditionally loading svg4everybody, if the browser is IE11:

const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
if (isIE11) {
  const s4e = document.createElement('script');
  const s4eUrl = 'svg4everybody.min.js'
  s4e.setAttribute('src', s4eUrl);
  s4e.onload = function() { svg4everybody(); }
Enter fullscreen mode Exit fullscreen mode

Download the script from svg4everybody's GitHub-repo, and replace s4eUrl with the correct path.
Or link to it directly from Cloudflare or a similar CDN-service.

While svg4everybody works in most cases, I had a project where <text>-elements were being added dynamically to an <svg>-shape, so editors could modify text-lines in an <svg>-shape (custom svg-splashes with product-offers) from a CMS:

<svg viewBox="0 0 100 100">
  <use xlink:href="file.svg#name" />
  Dynamic <text>content</text> here ...
Enter fullscreen mode Exit fullscreen mode

svg4everybody replaces <use> with the actual <symbol>-content, but it does that as the last chil_ of the parent:

<svg viewBox="0 0 100 100">
  <text>...dynamic content</text>
  Injected content here...
Enter fullscreen mode Exit fullscreen mode

Thus, the <svg>-shape ended up after the <text>-lines. 😞
As the project was already running Babel, I could write a poly-fill in ES6:

* @function svgUse
* @description Looks up <symbol> in external <svg>, adds content of <symbol> to local <svg>
* @param {Node} scope
export function svgUse(scope = document) {
const use = scope.querySelectorAll('use');
if (use) {
  let symbols = {};
  let svgs = {};
  /* Build Set of unique url's */
  const urls = [];
  use.forEach(elm => {
    const url = elm.getAttribute("xlink:href").split('#')[0];
    if (url && urls.indexOf(url) === -1) {
  /* Fetch all unique URL's, push to
  svgs-object with the url as key */
  Promise.all( url => {
    const data = await fetch(url);
    const text = await data.text();
    svgs[`"${url}"`] = text;
  })).then(() => {
    /* Process each <use>-element, lookup content in `svgs` */
    use.forEach(elm => {
      const [url, id] = elm.getAttribute("xlink:href").split('#');
      const exists = symbols.hasOwnProperty(id);
      /* Check if `id` is already created as a (data)-
      fragment in `symbols`, set fragment */
      let fragment = exists ? symbols[id].data : document.createDocumentFragment();
      /* If `id` does not exist, look it up in svgs using `url`, 
      parse it and extract childNodes */
      if (!exists) {
        const parser = new DOMParser;
        const doc = parser.parseFromString(svgs[`"${url}"`], 'text/xml');
        const svg = doc.getElementById(id);
        Array.from(svg.childNodes).forEach(node => {return fragment.appendChild(node)});
        /* Push fragment and viewBox to `symbols` */
        symbols[id] = { data: fragment, viewBox : svg.getAttribute('viewBox') };
      /* If parent-<svg> does not have a viewBox-
      attribute, add it from <symbol> */
      if (!elm.parentNode.hasAttribute("viewBox")) {
        elm.parentNode.setAttribute('viewBox', symbols[id].viewBox);
      elm.parentNode.replaceChild(fragment.cloneNode(true), elm);
  }).catch(function(err) {
Enter fullscreen mode Exit fullscreen mode

Implementations: CSS Icons

Sometimes a huge SVG-based Icon System might be unnecessary, if you just need a few arrows, a “close-icon” etc. There’s a classic article on css-tricks about the shapes of CSS.
I thought it could be a fun project to create a small library of CSS Icons, using some of these classic techniques, but also adding newer features like clip-path for creating even more icons using pure CSS:

clip-path is — like svg — vector-based. path is not supported for clip-path yet, but when it is, you’ll have nearly as many possibilities for creating shapes and icons as in svg.

If you want to create your own icons using clip-path, I’ve made a small tool for that:

Implementations: Icon Fonts

I think we've all encountered a slow-loading website, where strange letters or symbols were shown instead of an icon — until the Icon Font finished loading.

That's just one of the downsides of using an Icon font.
In the blog-post “It's 2019! Let's End The Debate On Icon Fonts vs SVG Icons”, <svg> is the clear winner when it comes to:

  • Accessibility. An <svg> is treated like an image, not as text, that will be read aloud by a screen-reader.
  • Scalability. This is the main reason why I switched to using <svg>. Also, they're easier to modify on-the-fly.
  • Animation. You can animate each path, line etc. in an <svg>

Google provides an Icon font for their Material Design, so the format is not completely dead — yet.

Thank you for reading!

Discussion (0)

Editor guide