DEV Community

Vesa Piittinen
Vesa Piittinen

Posted on

Icons for React & co, the web standards way thank you

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.

<svg id="icon" xmlns="" 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)"/>
Enter fullscreen mode Exit fullscreen mode

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:

<svg width="4rem" height="4rem">
    <use href="facebook.svg#icon"></use>
Enter fullscreen mode Exit fullscreen mode

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

With this code we can now style the icon itself:

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

:hover > svg {
    --icon-color: unset;
    --icon-light-color: unset;
Enter fullscreen mode Exit fullscreen mode

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 />.

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.


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.

Top comments (0)