Shadow Context and CSS variables trick for more straightforward SVG Icon handling.

Nota Bene: There is a better way to do this, see part 2 of the series:

Leveraging the shadow context for SVG icons, part 2

The method described here is interesting, and works fine for Design System icons. The method described in the part 2 is more flexible and explores how to achieve handle the SVG icons in production scenarios more elegantly.

I was figuring out the best way to recolor SVG icons for dark mode and pseudo-classes like :hover following a guide on the CSS-tricks1 when I realized I could just use Lit to inline an SVG file and use the currentColor method normally unavailable with externally stored SVG’s.

Quick googling revealed that I was correct and that someone else had already created a way to do almost the same2. While Patrick Walther’s solution has merits, it feels a tad complicated when I could just fetch when rendering.

The solution

My concept is pretty simple. It’s a custom element that looks like img tag, fetches an SVG file and renders it as an inline SVG. It also has a loading state that renders a default icon while the fetch is happening.

To get the code working, you need to have way to compile Lit to JS. My setup obviously uses Astro, but the code shoulf work fine with any Vite based build tooling. After that, using the element is as simple as <cn-icon src="./the-icon.svg"></cn-icon>

As an obvious note: we are fetching the SVG file (from browser cache) every time the element rendered, might not be ideal unless you are using a service worker and explicitly caching the SVG files.

The Code

import {customElement, property} from 'lit/decorators.js'
import {LitElement, css} from 'lit'
import {until} from 'lit-html/directives/until.js'
import {unsafeHTML } from 'lit-html/directives/unsafe-html.js'

@customElement('cn-icon')
export class CnIcon extends LitElement {
  public static styles = css`
    :host {
      color: var(--cn-icon-color, currentColor);
      height: var(--cn-icon-size, 128px);
      width: var(--cn-icon-size, 128px);
      display: block;
    }
   :host svg {
      height: 100%;
      width: 100%;
    }`

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

  @property({type: Boolean, reflect: true})
    small?: boolean = false

  loadingIcon = `<svg height="128" width="128">
    <circle cx="64" cy="64" r="32" fill="currentColor" />
  </svg>`

  defaultIcon = loadingIcon

  protected render() {
    const content = fetch(this.src).then(r => 
      r.status === 200 ? r.text().then(
        iconString => unsafeHTML(iconString)) : unsafeHTML(this.defaultIcon)
    )
    // fetch the icon and render it, or render a loading/default icon
    return until(content, this.loadingIcon)
  }
}

Footnotes

  1. Change Color of SVG on Hover (https://css-tricks.com/change-color-of-svg-on-hover/)

  2. Creating an icon web component with lazy loading in LitElement (https://software-engineering-corner.hashnode.dev/creating-an-icon-web-component-with-lazy-loading-in-litelement)