Introducing @svg-use

by Fotis Papadogeorgopoulos

Summary

This post introduces @svg-use, a set of tools and bundler plugins, to ergonomically load SVG files as components, via SVG’s <use href> mechanism.

First, we look at the current landscape for embedding SVG icons in JS frontends, and especially React components. For the most part, this involves inlining SVG as JS code (also called SVG-in-JS). We analyse the problem space in order to find the issues that inlining solves, and weigh its costs and benefits.

After that, we introduce <img src> and SVG’s <use href>, and where they fit in the problem space. We introduce the @svg-use toolchain as a way to make <use href> more ergonomic, in order to make it competitive with inlining.

Finally, we look toward the future, and investigate how future web standards might facilitate these patterns.

The core problem

A common technique in the JS (and especially React) ecosystem is converting SVG icons to components, so that they can be imported by JS code. One common library for this task is svgr, by Greg Bergé and contributors. It provides bundler plugins to facilitate converting SVG to JSX. Let’s call this approach SVG-in-JS, for the sake of comparison.

To give a concrete example, SVG-in-JS results in a file like this. Note how the SVG paths and shapes are all inlined:

const SvgComponent = (props) => (
  <svg width="1em" height="1em" viewBox="0 0 48 1" {...props}>
    <path d="M0 0h48v1H0z" fill="currentColor" fillRule="evenodd" />
    {/* and possibly other paths / shapes here */}
  </svg>
);

export default SvgComponent;

The SVG-in-JS approach is contrasted with referencing the SVG as an asset, and using it in img[src] or in svg > use[href]. Before we discuss those approaches, let’s look at what problems SVG-in-JS solves.

The first problem is theming 🎨. By including the SVG inline, one can use regular HTML attributes and CSS selectors, and inherit custom properties easily. Most often, this is done to inherit currentColor, but other more bespoke custom properties and theming schemes exist.

import SvgComponent from './icon.svg';

const MyComponent = () => {
  return <SvgComponent style="color: blue;" />;
};

Another problem is delivery and portability 📦. By lifting SVG into the realm of JS, it can be loaded via ES modules, same as any other JS code. This is not a particularly big deal for applications, which typically use bundlers that are capable of referencing assets in the module graph. However, when it comes to shareable libraries, this provides a delivery mechanism that works anywhere that JS is supported, without configuration on behalf of the user. This has the caveat that you need a JSX runtime, such as React, but then again icon libraries are often published in the context of a certain framework.

In general, referencing assets (such as images or even CSS) from JS is currently not standardised. It is thus hard to ship reusable libraries that depend on assets, at least in a general way, i.e. one that does not assume a specific bundler.

While this has been true historically, and there is still no standardisation for asset loading, there is a technique that works with (most) current bundlers and all current web browsers. This is called the “new URL scheme” (web.dev article by Ingvar Stepanyan):

// This is resolved relative to the JS file, instead of the document
// bundlers resolve this as well
const svgUrl = new URL('./path/to/svg.svg', import.meta.url);

The above technique only solves the delivery 📦 problem though, and does not solve the theming 🎨 problem.

The SVG-in-JS approach is thus appealing, because it solves real problems in a relatively simple way. However, it also comes with some drawbacks.

Drawbacks of SVG-in-JS

By inlining SVGs in JS, we are incurring a number of runtime costs.

In short:

  • Each component’s code is parsed multiple times: first as JS, then as HTML when inserted into the document. This happens for every instance of the component.
  • Each SVG icon is duplicated in the DOM for every instance, bloating the DOM size.
  • The size of the SVG icon adds to the JS bundle size. Some common SVG icons can be large, for example country flags with intricate designs. On a similar note, it is also easy to accidentally inline large SVGs.

All of the points above lead to a delay in meaningful interactivity metrics. This article by Jacob ‘Kurt’ Groß dives into the different drawbacks of SVG-in-JS, as well as different alternatives.. I highly recommend reading through that article, to understand the tradeoffs.

I believe that the runtime costs are big enough for many common cases, to warrant an alternative.

A first alternative: <img src>

A simple approach would be to use an <img src>, and evaluate how it stacks up against our criteria.

We can use images as follows:

// Current bundlers resolve the URL to a file (or can be configured to do so)
import iconUrl from './path/to/icon.svg';

// Both bundlers and browsers can resolve this, as above
const anotherIconUrl = new URL('./path/to/svg.svg', import.meta.url);

const MyComponent = () => (
  <>
    <img src={iconUrl} alt="" />
    <img src={anotherIconUrl} loading="lazy" alt="" />
  </>
);

This solves the delivery 📦 problem effectively, both for applications and shareable libraries (via the new URL scheme). The image itself does not exist in the JS bundle, effectively solving the performance considerations.

The img element is one of the most robust elements, and provides a bunch of flexibility to tune the end-user delivery, such as loading and fetchPriority. The img element can also load cross-origin resources. I consider the img element the benchmark, when it comes to delivery.

Unfortunately, the img element does not address the theming 🎨 problem at all. There is no inheritance of CSS to speak of, so any colours that are present in the original SVG, are those that will be shown to the end-user.

Thus, the img element is useful for multi-color SVGs (such as country flags, which I see inlined quite often :/), but is not very useful for UI icons, that should be flexible and themeable.

(This might change in the future; more on that later.)

SVG’s <use href> mechanism

SVG provides the <use> element, which can reference same-origin external SVGs via the href attribute.

Disregarding JS and React for a moment, if we were to reference an SVG with use in HTML (the “user code”), it would look like this:

<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <use href="https://example.com/icon.svg#someId" style="color: blue;"></use>
</svg>

To make the above work, we need a few moving parts:

  1. (optional) a viewBox, to allow intrinsic sizing of the outer svg.
  2. a url to reference the external SVG by.
  3. an id to reference the external SVG by (while SVG 2 allows referencing without an id, that part does not seem supported in browsers).
  4. a themeing system, to allow us to customise the referenced SVG. This can be done with currentColor (for monochromatic icons) or via CSS custom properties, which can be inherited by the referenced SVG.

The SVG itself (the “asset”) looks like this. Crucially, it is not inlined into the JS bundle, but exists on its own, similar to the img[src] example above:

<svg
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 24 24"
  id="use-href-target"
  stroke="currentColor"
  fill="none"
  stroke-width="2"
>
  <line x1="5" y1="12" x2="19" y2="12" />
  <polyline points="12 5 19 12 12 19" />
</svg>

The core thesis is that the above setup is desirable in terms of its runtime characteristics (more on that in a moment), but is more tedious to set up than SVG-in-JS, due to the lack of a dedicated toolchain.

The @svg-use libraries consider the above structure (the “user code” and the “asset”) as a compilation target, and provide a toolchain for linking the two together to your JS code. Before we talk about the implementation, let’s evaluate this solution against the problem space.

Pros and cons of <use href>

By referencing an external asset, we are avoiding the double-parsing and JS bundle size costs; we only need to ship a URL and some metadata to the JS bundle, as well as a wrapper component (for convenience). We are also reducing the DOM size, since a single use is smaller than most icons, and involves fewer elements.

Theming 🎨 is achieved via a transform on the SVG, and can often be as simple as passing down currentColor. CSS custom properties and currentColor are inherited as expected. The theming transform is done statically, and has no runtime cost.

The portability 📦 in this approach is good, because you can use the resulting SVGs directly, and not just in React. You could even write out the <use href> manually if you wanted, or create your own wrapper component, in your framework of choice.

Delivery of shared libraries can be reliable, using the new URL scheme as a transform target. The svg-use repository has a relevant example.

One downside, in terms of delivery, is the lack of Cross-Origin Resource Sharing (CORS) for SVG <use href> references. This is a real issue, that can only be reliably solved at the specification level. However, SVG-in-JS is often used for local and shared-library SVG use-cases, which are hosted on the same origin, so the lack of CORS is not an issue for replacing them.

In case you use a Content Delivery Network (CDN) for your application assets (including JS an SVG), you need some mechanism to rewrite the URLs at build time or at runtime, to point them to a same-origin proxy. This can be more or less efficient, depending on the implementation, and your threshold for accepting that tradeoff might be different to mine. The default @svg-use/react components enable this functionality.

In terms of loading strategies, use is not as flexible as img. For example, there is no fetchPriority or loading specified. This is about on par with inlining, which is eager (and synchronous) by default, and is made lazy via JS import() calls and related mechanisms. If you have icons that need to be displayed synchronously, then inlining might be the better approach.

All that said, there are cases where inlining the SVGs or using img[src] is the better or simpler approach, depending on your loading patterns. I do not claim that the <use href> pattern solves every scenario, but even if it leads to half of the SVGs no longer being inlined in JS “by default”, it will be a good step.

The core solution in @svg-use

Going back to JS and React, assuming some bundler config (webpack, rollup, vite), developers would consume the <use href> structure like this (assuming some bundler config):

import { Component as ArrowIcon } from './arrow.svg?svgUse';

const MyComponent = () => {
  return (
    <button>
      <ArrowIcon color="currentColor" role="img" aria-label="Continue" />
    </button>
  );
};

The arrow.svg SVG is transformed and made compatible with <use href> automatically.

Developers can also use reusable libraries, without having to configure their bundler. The libraries use the @svg-use tools as static transforms, emitting the new URL pattern for the href under the hood.

import { ArrowIcon } from 'my-shared-library';

const MyComponent = () => (
  <button>
    <ArrowIcon color="currentColor" role="img" aria-label="Continue" />
  </button>
);

In depth

When you write this:

import {
  Component as ArrowIcon,
  // not necessary to import; shown for demonstration
  href,
  id,
  viewBox,
} from './arrow.svg?svgUse';

const MyComponent = () => {
  return (
    <button>
      <ArrowIcon color="currentColor" role="img" aria-label="Continue" />
    </button>
  );
};

With a source arrow.svg of:

<svg
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  stroke="#111"
  fill="none"
  stroke-width="2"
>
  <line x1="5" y1="12" x2="19" y2="12" />
  <polyline points="12 5 19 12 12 19" />
</svg>

A bundler-specific plugin starts the following chain:

  1. The plugin resolves ./arrow.svg relative to the file and invokes @svg-use/core

    1. Core parses ./arrow.svg, to ensure that it fulfils the invariants
    2. Core extracts the id and viewBox of the top-level SVG element
    3. Core runs a customisable theming transform to turn the SVG element’s fills and strokes into configurable CSS custom properties (or currentColor)
    4. Core returns the transformed SVG content, and the extracted information
  2. The plugin emits the transformed SVG as an asset (using the bundler’s logic), and resolves its would-be URL.

  3. The plugin passes the URL to Core, to create a JS module. This is what the userland code ultimately sees.

  4. The module exports the extracted properties, and passes them to a “component factory”, for convenience.

  5. The plugin passes on the JS module to the bundler.

The transformed SVG asset (/assets/arrow-1234.svg) is this:

<svg
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 24 24"
  id="use-href-target"
  stroke="var(--svg-use-href-primary, currentColor)"
  fill="none"
  stroke-width="2"
>
  <line x1="5" y1="12" x2="19" y2="12" />
  <polyline points="12 5 19 12 12 19" />
</svg>

The ad-hoc JS module is the equivalent of this:

import { createThemedExternalSvg } from '@svg-use/react';

export const id = 'use-href-target';
export const href = new URL('/assets/arrow-1234.svg', import.meta.url).href;
export const viewBox = '0 0 24 24';

/* createThemedExternalSvg is a component factory function */
export const Component = createThemedExternalSvg({ href, id, viewBox });

This approach combines convenience (using Component directly), with composition (such as using href and id to build your own wrapper components, using href for preloading etc.). Refer to the svg-use repository for more details on customisations, such as the themeing transform.

The core utilities are composable and easy to extend. Additionally, type safety and user convenience are key; this should be as (or nearly as) convenient as SVG-in-JS.

The future

I created @svg-use because I saw a gap in the tooling for adopting the <svg use> pattern, in a contemporary JS context. However, my dream is that web standards will pick up some of the rough edges, so that this library will eventually become obsolete (or simply nice-to-have).

Here are some proposals that I am aware of, that could change the calculus around this library. I am ordering them from “nice to have” to “existential”.

JS import attributes

JavaScript supports import attributes, which allow passing metadata about how a module should be loaded.

In the previous example, you might have noticed a pesky ?svgUse query parameter suffix in the import. This is used to disambiguate the loading of SVGs in bundlers, between those that use @svg-use and others.

Import attributes would make it marginally nicer to write imports:

import { Component as Arrow } from './arrow.svg?svgUse&theme=none';
import { Component as Arrow2 } from './arrow-2.svg' with { type: 'svgUse', theme: 'none' };

Import attributes exist in current browsers, except Firefox. This is not a big deal, because bundlers can transpile them away, and just pass the metadata to the plugins.

However, I am avoiding them because TypeScript does not yet expose type declarations based on import attributes. Thus, to get type-safety, we still use the string-based approach, on the module name.

Do give your thoughts on the tracking issue, if you have them!

JS asset references

The new URL pattern is neat for referencing relative assets, but it works mostly based on convention. Bundlers treat this as special syntax for resolving assets, but I would love a more concrete syntactic construct, to avoid ambiguities. For example, esbuild does not support the new URL pattern, with some concerns around standardisation.

There was (is?) a proposal for asset references in JavaScript, but it seems to have stalled. Also, it seems to have been written at a time when import attributes did not exist, but I might be wrong.

There is some other work being done around imports, such as source phase imports. While that does not help with assets by itself, I have a gut feeling that the renewed interest in ES loading mechanisms could lead to a proposal for assets down the line.

References with import.meta.resolve

On the topic of more clear syntactic markers for resolving relative paths, there is the import.meta.resolve built-in function.

This function resolves a module specifier to a URL, with the current module URL as a base. The resolved URL does not have to resolve to an actual module, which means we can just as easily reference non-JS assets.

In other words, these two invocations would be equivalent:

const assetPath = new URL('./arrow.svg', import.meta.url).href;
const assetPath2 = import.meta.resolve('./arrow.svg');

There are some other subtleties when it comes to resolving bare module specifiers, but that does not make a difference for our use-case. Browser support for import.meta.resolve is not as wide as new URL, but is still widely supported in current browsers.

My gut feeling is that import.meta.resolve could replace new URL as the build target for svg-use in the near future, because it seems like a clearer signal of an import dependency. I’m eager to see what bundlers and non-browser environments do with this.

SVG crossorigin use

If SVG supported crossorigin usage, then shipping shared icon libraries on CDNs would be much simpler, without having to proxy/rewrite the URLs (and avoiding breaking things accidentally). This seems to have stalled, but I might be wrong.

SVG 2: referencing SVGs without an id fragment

According to SVG 2, you could do this:

<svg>
  <use href="arrow.svg"></use>
</svg>

…and have it reference the top-most SVG. This seems specified, but not implemented.

This would simplify the need for an id, and extracting it statically.

CSS Linked Parameters

If CSS Linked Parameters lands, we will be able to pass custom properties to SVGs inside of img[src]. (I am omitting the syntax, because it is in flux).

This avoids the setup with svg > use[href], in favour of the much simpler img element. As mentioned previously, the img element is really robust in terms of loading and delivery. It can already be loaded cross-origin, so this possibly avoids the CDN issues. Since an SVG img is displayed as a whole, this also avoids the id issue.

This is the most promising proposal. If it lands in browsers, then this library will mostly be useful for two things: themeing (taking an ad-hoc svg and bringing it into a system), and extracting the intrinsic aspect ratio (the viewBox, essentially). The wrappers will adjust their internals, but will likely not need to change their external interface (hooray!)

This GitHub issue by Lea Verou has more details around CSS Linked Parameters and icons.

Wrapping up

We covered a lot of ground in this post!

As a summary, we looked at the challenges of delivering assets in JS, specifically the delivery of SVG icons. We looked SVG-in-JS techniques, and weighed their pros and cons, in terms of delivery, theming, ergonomics, and runtime performance. We then looked at the alternatives of <img href> and <use href>, with a similar lens.

Finally, we introduced @svg-use, a toolchain to make the <use href> pattern more ergonomic and consistent. We also looked at future web specification developments, that could change the calculus around these patterns.

Please get in touch if you have thoughts about all of this! I am especially interested to hear your use-cases in your applications, and any rough edges that you might spot.

Head on over to the svg-use repository to get started. Happy coding!

Webmentions

No replies yet.

Tagged under