How to implement and draw external SVG sprites
Tags: javascript, svg, webpack, react
Published: 2023-02-25 17:58:14

Hello dev.to community!
This is my first post so I'm happy to start to share my experience (hope this will be useful for somebody) with SVG sprites here.

**The problem**

In my project I have many SVG icons (around 600 icons), so I decided to use SVG sprites to improve performance. A bundle size is critical in my case, so we need to implement external SVG sprites, so that they will load as a separate files.

**SVG sprites implementation**

To achieve that I used svg-sprite-loader library [svg-sprite-loader](https://github.com/JetBrains/svg-sprite-loader).

First of all you should install the library as dev dependency:
`npm install svg-sprite-loader --save-dev`

All Webpack config code snippets provided for Webpack 4.

Initial Webpack config will look like this:

```javascript
{
    test: /\.svg$/,
    use: [
      {
        loader: 'svg-sprite-loader',
        options: {
            extract: true,
            publicPath: 'assets/sprites/',
            spriteFilename: svgPath => `sprite${svgPath.substr(-4)}`
         }
       }
     ]
}
```

In the extract mode of the loader, it is possible to create as many sprites as you like. We have many icons in the project, so having one sprite for all the icons is not the best choice.
We can update webpack config to support multiple sprites.

For a folder structure:

```javascript
src/
    svg/
        sprite1
        sprite2
```

webpack config will look like this:

```javascript
{
    test: /\.svg$/,
    include: ['sprite1', 'sprite2'].map(folder => path.resolve(__dirname, 'src', 'svg', folder)),
    use: [
      {
        loader: 'svg-sprite-loader',
        options: {
          extract: true,
          publicPath: 'assets/sprites/',
          spriteFilename: svgPath => {
            for (const folder of ['sprite1', 'sprite2']) {
              if (svgPath.includes(path.resolve('src', 'svg', folder))) {
                return `${folder}.svg`;
              }
            }
          }
        }
      }
    ]
}
```

To use a sprite I need to import it to our component:

`import pencilIcon from 'svg/sprite1/pencil-icon.svg';`

`pencilIcon` will have three properties:

```javascript
{
    id: "pencil-icon",
    url: "svg/sprite1.svg#pencil-icon"
    viewBox: "0 0 50 50"
}
```
Please note that `url` property is available in the exact mode only.
 
To show the icon, we need to do next:

```javascript
<svg viewBox={pencilIcon.viewBox} width="50" height="50">
    <use href={pencilIcon.url}/>
</svg>
```

That works just fine!

What if we'd like to draw this icon on Canvas?

We can use Image element and set a sprite url as a source: 

```javascript
const image = new Image();
image.src = "svg/sprite1.svg#pencil-icon";
image.onload = () => {
    this.image = image;
}
const ctx = canvas.getContext('2d');
ctx.drawImage(this.image, 100, 100, 50, 50);
```

It works fine in Chrome, but not in Safari and Firefox.

As a workaround here, we can load the whole sprite file by url and find an icon:

```javascript
function loadSprite(url: string) {
  const response = await fetch(url);
  const xml = await response.text();
  const parser = new DOMParser();

  return parser.parseFromString(xml, 'image/svg+xml');
}
```

To parse the icon we can do next:

```javascript
function getSvgIconFromExternalSprite(source, width, height) {
  const spriteFile = loadSprite(source.url);
  const symbols = Array.from(spriteFile.childNodes[0].firstChild.childNodes);
  const sprite = symbols.find(node => node.id === source.id);

  sprite.setAttribute('width', width);
  sprite.setAttribute('height', height);

  const spriteText = sprite!.outerHTML.replaceAll(/symbol/g, 'svg');

  return `data:image/svg+xml;utf8,${encodeURIComponent(spriteText)}`;
}
```

I set width and height explicitly, because of the known Firefox issue (https://bugzilla.mozilla.org/show_bug.cgi?id=700533)

**Summary**

External SVG sprite can be helpful if you have a lot of icons and bundle size is critical in your case. I hope my post can help somebody to use them in your project.

Good code everyone!