DEV Community

Cover image for Neater Icons with Web Components
Ben Taylor
Ben Taylor

Posted on

Neater Icons with Web Components

Over the years we've seen steady changes in best practices for rendering icons. Icon fonts remain easy to use, SVGs render well on high definition screens and well, for some reason Facebook seems to still use a png sprite sheet?

Facebook uses PNG sprite sheets for their icons

(I'm sure they have a good reason, they're smart people)

However the actual use of these different techniques still feels... imperfect. If you're using icon fonts you'll be writing some HTML like this:

<button>
  <span class="icon-plus"></span> Add
</button>
Enter fullscreen mode Exit fullscreen mode

Or you might inject the icon from your CSS with something like this:

<button class="add">Add</button>
Enter fullscreen mode Exit fullscreen mode
.add::before {
  font-family: 'iconfont';
  content: '\addicon';
}
Enter fullscreen mode Exit fullscreen mode

If you're using SVG you could just drop that SVG straight into the DOM (not a bad idea).

<button>
  <svg viewBox="0 0 16 16">
    <path d="big series of numbers"></path>
    <path d="maybe another path"></path>
  </svg>
  Add
</button>
Enter fullscreen mode Exit fullscreen mode

But more likely you're using an SVG sprite sheet and so your code looks a bit tidier, like this:

<button class="btn">
  <svg viewBox="0 0 16 16">
    <use xlink:href="#icon-add"></use>
  </svg>
  Add
</button>
Enter fullscreen mode Exit fullscreen mode

And then if you're using a PNG sprite sheet... uhh lets not go into it.

My main point is that you end up mixing the implementation of your icon rendering system, with the markup and CSS you're writing to implement your webpage. This isn't necessarily bad, but abstraction can create neat boundaries. If this was JavaScript we would have long ago written a helper function like icon('name') that returns us an icon with that name.

On the web we have a great new friend for abstraction: Web Components.

With Web Components we could write this code example as:

<button>
  <my-icon name="plus"></my-icon>
  Add
</button>
Enter fullscreen mode Exit fullscreen mode

This lets us hide the implementation details of our icon rendering system and allows for a short and semantic syntax in its place. If someone unfamiliar with your codebase had a read of that, they'd think "Hey this thing renders an icon".

This is a great way to use Web Components - to hide the implementation details of something you do all the time. You don't have to build a huge design system to reap these rewards. Just write a bit of JavaScript.

Speaking of writing JavaScript - it's probably time I showed you how to implement this element. I'm going to show it here without any frameworks or magic, but feel free to npm install your way to a solution that works for you. I'll be showing you the SVG version - but you could use a similar strategy with icon fonts.

Implementing our Icon

First off you'll need a template element. This will represent the stuff that lives "inside" the my-icon element.

<template id="my-icon">
  <svg>
    <use id="use" xlink:href=""></use>
  </svg>
</template>
Enter fullscreen mode Exit fullscreen mode

That wasn't so scary. Here we're defining a template element which can be used inside our custom element. I've used an id for the use element so we can set its xlink:href later. Since the id is inside a template it won't conflict with the rest of the document.

Then in JavaScript we create a custom element.

// URL to your SVG
const baseURL = '/sheet.svg';

class MyIconElement extends HTMLElement {
  // This tells the browser we want to be told
  // if the `name` attribute changes.
  static get observedAttributes() {
    return ['name'];
  }

  constructor() {
    super();

    // Here we create the DOM elements from the template
    // and put them in the ~~spooky~~ shadow DOM.
    this.attachShadow({mode: 'open'});
    const template = document.getElementById('my-icon');
    const clone = template.content.cloneNode(true);
    this.shadowRoot.appendChild(clone);

    // Lets also grab a reference to that use element
    this.useEl = this.shadowRoot.getElementById('use');
  }

  // This is called whenever an attribute in the
  // observed attributes changes. It means you can
  // change `name` and it will update.
  attributeChangedCallback(name, oldValue, newValue) {
    this.useEl.setAttribute('xlink:href', `${baseURL}#icon-${newValue}`);
  }
}


// Finally lets define this custom element
customElements.define('my-icon', MyIconElement);
Enter fullscreen mode Exit fullscreen mode

And we're done!

We can now write:

<button>
  <my-icon name="plus"></my-icon>
  Add
</button>
Enter fullscreen mode Exit fullscreen mode

To have it render like this:

Alt Text

Understanding that JavaScript

There's a lot going on in this JavaScript so let's talk about it a bit. If you haven't seen the ~spooky~ shadow DOM before it can seem a bit scary. A better name for it would be a "private" DOM. It's a small little private space for you to set styles, create elements, and do weird things without impacting the "public" DOM. Or, if you like, a dark shadow realm where you can banish horrors of CSS and HTML - and nobody will know.

A big benefit of using the shadow DOM is that we can pollute our namespace with id all we like, it won't impact anyone else. If you look at the page in the inspector, the template SVG will all be hidden. You can still view it - but it's not there by default.

Alt Text

Then if we expand the shadow root.

Alt Text

Another trick here is in the attributeChangedCallback function. Here is the real "work" of this element. It turns the icon name that a human would use, into a xlink:href friendly value that we then set on the use element.

If we changed the name attribute to be "cross" as an argument then the HTML would end up looking like this:

<svg>
  <use id="use" xlink:href="sheet.svg/#icon-cross"></use>
</svg>
Enter fullscreen mode Exit fullscreen mode

Basically the same as before!

You can see a demo of this, with two different icons in this example. I've also made it a codepen here but the SVG won't load due to cross-site protections.

If instead you wanted to use icon fonts, then you could change the attributedChangedCallback to set the properties you need for your icon font.

Making it useful (extensions)

So at this point you're basically done, you have icons on the page - what more could you want? Well, probably a lot more. You'll want to set up some default CSS, you may want to add some accessibility defaults. Maybe there's some helpers specific to your application you could add.

There's plenty of guides out there about how best to do SVG icons, so I'll leave the details to them. However here's some quick tricks to make things work well. We'll revisit our template from earlier:

<template id="my-icon">
  <!-- Web components can have a scoped style tag, this only impacts elements inside the shadow DOM. -->
  <style>
  /* :host indicates the "host" element of the
   * shadow DOM. So our custom my-icon element.
   */
  :host {
    display: inline;
  }

  /* Because this is scoped, we can use very
   * simple selectors.
   */
  svg {
    width: 1em;
    height: 1em;
    fill: currentColor;
  }
  </style>

  <svg>
    <use id="use" xlink:href=""></use>
  </svg>
</template>
Enter fullscreen mode Exit fullscreen mode

Here I've added a style element into the template. In this context style elements are not considered a code smell - in fact they're necessary. This style element is entirely scoped to the Shadow DOM and lets us be "lazy" in our CSS. This scoped approach is one of the greatest abstraction tricks Web Components can give us, when you're writing CSS here you don't have to think "how will this impact my other elements and classes" you only need to keep the one scope in your head.

The downside and upside of using the Shadow DOM is that regular CSS can't "penetrate" it. You start off with everything at browser defaults - so if you want any styles you'll have to set them here. Some things will inherit through the boundary, mostly text styles, but you can't use any classes you've set up outside the Shadow DOM.

In my style here I've set the :host to be display: inline - this is mostly to indicate "this is an inline element". Next I've added some simple styles to the svg element. These styles ensure it resizes as the font-size increases, and changes its fill to whatever color is set.

Now if you use these icons as part of your website, in buttons, links etc - they fit in. They resize and change colour based on their context. This is extremely smooth, and gives you many of the benefits of icon fonts but with SVG, a much easier to work with format.

Final Note

I'm new to publishing on dev.to if you like this and you'd like to see more things by me just shoot through a comment or message!

Further Reading

Icon System with SVG Sprites on CSS Tricks - great introduction to using SVG Sprites.

Accessible SVG Icons with Inline Sprites on 24a11y - fantastic article about making SVG icons accessible.

An Introduction to Web Components on CSS Tricks - good place to start learning about Web Components.

Web Components on MDN - reference for Web Component information.

Top comments (1)

Collapse
 
dannyengelman profile image
Danny Engelman

I took it one step further with iconmeister.github.io