Leveraging Web Components for SVG Icons – Part 2

I recently had an idea on how to leverage the shadow context for SVG icons . In this article, I explore how to expand the concept for a more generic solution.

The fast pace of the web platform is both a blessing and a curse. In recent years, we have seen a lot of new features being added to HTML and CSS. And it feels that the humble SVG has been left behind. Yes, we can use modern CSS and Javascript to manipulate SVGs, but it feels a bit hacky and could be a lot more intuitive.

A common way to overcome some of the limitations of SVG is to inline them to the HTML document. Often, this is done by creating a platform-specific component that wraps the styling and handling of the SVG icons in a reusable way. Popular frameworks like React or Vue have multiple open-source plugins that do just that, which would be great if every other project would not discard the plugins and try to reinvent the wheel.

This had me thinking: If we often end up inlining the SVG with a component at the rendering time, why not do it in a way native to modern web. And, incidentally, create some additional benefits as we do.

In other words, why not reinvent the wheel – again?

TL;DR: its a rather long read. If you just want the code - you can jump to the solution or check the code from a dedicated example repo I published on GitHub.

Inlining external SVG's

Discussing my previous article with a colleague, he pointed out that the SVG spec provides a way to inline external SVGs with the <use href=... /> element. Inlining the icon this way lets us use currentColor, which is widely supported in browsers, as long as the icon has an ID.

Assuming we have a simple SVG file containing a circle with an id icon:

<svg
  version="1.1"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  viewBox="0 0 128 128">
    <circle id="icon" fill="currentColor" cx="64" cy="64" r="64" />
</svg>
The SVG code used in the examples.

We can use it in our HTML in multiple ways. We could add it as an external image or SVG and lose the currentColor support. Or we could inline it with the <use href=... /> element in an SVG inside the HTML document. This way we the icon payload is part of the DOM and we can use currentColor to style it.

External SVG inlined with <use href=... />.
The same SVG as above, but as an external image
External SVG as an image.

In the example above, both grid columns are styled with style="color: #ff2fb2" and load the same external code. Inlining the code with <use applies the currentColor from the parent element while loading the SVG as an image does not. This makes a lot of sense: when loading an SVG as an external image, we expect it to work precisely as a WEBP or PNG file would.

Inlined SVG + use

Looking at the code below, the solution based on the SVG use element is straightforward and easy to understand. However, it is verbose and requires us to repeat the mandatory attributes like viewBox every time we use the icon. And while I often see striving for DRY code as waste, the approach makes sense here. Encapsulating the SVG tag to a component will reduce errors and simplify modifying it in the future.

<svg
  version="1.1"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  viewBox="0 0 128 128">
    
  <use href="/sundry/src/example-icon.svg#icon" />
    
</svg>
The inline SVG code used above

Isn't this what the Web Compoonents were supposed to do?

When most developers use big frameworks like React or Angular, it is hard to explain or grasp the importance of Web Components. With Web Components, we can extend the HTML native to the browser by creating a new element that acts and behaves exactly as an HTML element would. The resulting component is not only portable between frameworks and projects - using it is intuitive and easy for anyone who has worked with HTML.

Example

A Web Component lets us define an internal DOM structure and default styling to an element. Here, we use the functionality to DRY the mandatory SVG attributes and add some styling to make the icon work as a standard inline-block element.

External SVG inlined with <use href=... />.
The Web Component <svg-icon src="... />.

As we can see, The Web Component works precisely as the inlining. It is a lot more concise and intuitive to use. And as it is native to the browser, we can style it with CSS and handle it with Javascript as we would any other element.

Conclusion: The working solution

I'm a big fan of Lit. It's a top-notch library that – together with Vite – makes writing web components simple and fast. Scoping styles, rendering the DOM and handling props just work.

Below is a minimal example element written with Lit. It shows the essential concepts and can easily be extended for most scenarios.

import { LitElement, css, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'

/**
 * An example of SVG Icons wrapped in a LitElement.
 *
 * @src [string] the path to the SVG file
 * @name [string], optional, the ID of the SVG icon, defaults to "icon"
 */
@customElement('svg-icon')
export class SvgIcon extends LitElement {

  @property({ type: String, reflect: true })
  src = ''

  @property({ type: String, reflect: true })
  name = ''

  render() {
    const sourcePath = this.src + '#' + (this.name || 'icon')

    return html`<svg
      version="1.1"
      xmlns="http://www.w3.org/2000/svg"
      xmlns:xlink="http://www.w3.org/1999/xlink"
      viewBox="0 0 128 128">
      <use href="${sourcePath}"/>
    </svg>
    `
  }

  static styles = css`
    :host {
      display: inline-block;
      width: 1rem;
      height: 1rem;
    }
    :host svg {
      width: 100%;
      height: 100%;
    }`
}

declare global {
  interface HTMLElementTagNameMap {
    'svg-icon': SvgIcon
  }
}

The element above highlights perfectly the power of Web Components. We now have a universal extension to HTML that acts exactly as you would expect an HTML icon element to. Creating A11y functionality, default styling, and icon specifics can be added to the component. Any overrides can be done without more understanding of the element internals than we have for buttons, fields, or links.