Icons for React & co, the web standards way thank you
Tags: webdev, react, css, html
Published: 2021-10-31 16:40:53

These days there are a **LOT** of solutions to the icons problem. You can simply go ahead and pick a solution you like and use it.

However there is one hickup in this: almost all the solutions these days are based on inline SVGs. This means a few things:

1. Icons are rendered to HTML when doing SSR. Very nice for initial render!
2. Icons are rendered to DOM on client-side.
3. Client-side render is based on components, such as ones based on Virtual DOM in React. Some solutions like Iconify use `dangerouslySetInnerHtml` which is slightly better (but then no SSR).
4. The icons are JavaScript code, or rely on JavaScript to function.

The good thing with inline SVG is that you have full control over the internals of SVG file. Need to change a color? Just set `fill` and you're good to go. Heck, since "it is just code" you can even write some of your own code to do modifications to the SVG before rendering it.

But when icons are code there are performance implications. You need to ensure icons are only included in your bundles when the icons are needed on page. So then you need to setup lazy loading of these components, at which point you start getting awful amount of complexity since on SSR you want to have the icons included in HTML, but then you have the icon also included in the client JavaScript bundle only for the sake of things matching on hydration. So you're serving the icons twice and doing JS processing for them. On SPA page loads you only load icons you need, but they're still code which is more expensive than pure image asset.

Can we do better?

## The Options

There are many ways to embed SVG images to pages. The reason inline SVG "won" in the past was due to being compatible with old browsers like Internet Explorer 11 while allowing CSS access to changing colors. The other means to embed SVG include solutions like `<img />`, `<object />`, CSS background or mask, and `<svg><use /></svg>`. Of these `img`, `object` and CSS embeds do not provide any further access to the SVG internals: there is no way you can control the style of an SVG file.

But there is one technique that **allows** you to style the internals: the `<use />` element. But you don't get direct access to styling via `fill` or `stroke`. Instead you only have access through indirect access via things like `currentColor` or CSS variables. The latter means no support for the old browsers, evergreen it must be.

Interested? Lets have a look!

## Gotchas of `<use />`

The nice thing about `<use />` is that you can give a `href` of any kind. So you can switch between two ways to embed: a regular URL path to SVG file, or inline the SVG using `data:` URL. This provides some flexibility! Also, since we are declaring an actual `<svg />` element in our HTML we can also provide a `<title />` element to ensure accessible context.

The downside is that unlike `<img />` element there is no native lazy loading available. This sucks and you're forced to implement lazy loading through some form of JavaScript.

To make this method work well there are a bunch of requirements:

1. You must give element an id, such as `id="icon"`.
2. You **must not** have `width` or `height` defined in `<use />` element, or in the actual SVG file.
3. To allow manipulating color you have to replace all `fill` and `stroke` values in the SVG with `currentColor`.
4. Better yet, you can pick a CSS variable name like `--icon-color` and use that for all the colors.

## Sample code

Here is an adapted Facebook logo with minimalistic code.

```html
<svg id="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
	<path d="M40,0H10C4.486,0,0,4.486,0,10v30c0,5.514,4.486,10,10,10h30c5.514,0,10-4.486,10-10V10C50,4.486,45.514,0,40,0z" fill="var(--icon-color,#1877f2)"/>
	<path d="M39,17h-3 c-2.145,0-3,0.504-3,2v3h6l-1,6h-5v20h-7V28h-3v-6h3v-3c0-4.677,1.581-8,7-8c2.902,0,6,1,6,1V17z" fill="var(--icon-light-color,#fff)"/>
</svg>
```

A notable thing here is that we've made a two color icon. It displays correctly and uses Facebook brand colors when viewed normally in a browser. I've chosen here to use `--icon-color` for the "main" color, and `--icon-light-color` to basically indicate a white color. Using two different variables lets us manipulate both via CSS individually.

The HTML is a rather short:

```html
<svg width="4rem" height="4rem">
	<title>Facebook</title>
	<use href="facebook.svg#icon"></use>
</svg>
```

Not really that much more code than with an `<img />` tag :)

With this code we can now style the icon itself:

```css
svg {
	--icon-light-color: rgba(255, 255, 255, 0.75);
	--icon-color: currentColor;
}

:hover > svg {
	--icon-color: unset;
	--icon-light-color: unset;
}
```

Here is a demonstration as an extended Codepen with comparison against `<img />` to show that you can't control color when embedding with it, but can via `<use />`.

{% codepen https://codepen.io/Merri/pen/mdMBBrR %}

## Color transitions

There is still one more thing that people might like to control: animating the colors. With this feature we're still limited, because only Chromium based browsers support `@property` which allows us to animate CSS variables as colors. I actually implemented this to the Codepen above (see style element in the HTML), but for some reason it didn't work there. I don't know why, maybe data URLs? I did get this to work while experimenting outside Codepen.

## Summary

So what do we get by doing things this way?

- No layout shifting as icon always has the space it needs reserved for it
- A much more web standards take: HTML remains the same before and after hydration
- Icons are resources, not code, and you can't fail with that
- No icon contents duplication to JS bundles (except if used as data URL)
- You can inline or use as external, no changes or special cases to the actual icon component implementation
- Color can be manipulated if icon uses `currentColor` or CSS variables

But what is not so nice?

- All current icons out there are incompatible: you need `id="icon"` in the SVG file
- Less pure fine grained control via CSS: everything has to be CSS variablized
- Color transitions lack full cross-browser support
- Lazy loading requires JavaScript
- There is no community or any kind of standardization around this solution

So what do you think? Is this worth more effort? There are many ways one could set this up, but building a helpful structure and process around this would need quite a bit of work. Any attempt for a successful adoption would need creating components for multiple frameworks and provide preprocessed SVG icon files.