How to Create SVG Sprite With Icons

Written by gmakarov | Published 2023/12/23
Tech Story Tags: frontend | svg | webpack | vite | how-to-create-svg-sprite | what-is-svg-sprite | how-to-create-icons | hackernoon-top-story | hackernoon-es | hackernoon-hi | hackernoon-zh | hackernoon-fr | hackernoon-bn | hackernoon-ru | hackernoon-vi | hackernoon-pt | hackernoon-ja | hackernoon-de | hackernoon-ko | hackernoon-tr

TLDRDevelopers often insert SVG directly into JSX. This is convenient to use, but it increases the JS bundle size. In the pursuit of optimization, I decided to find another way of using SVG icons without cluttering the bundle. We will talk about SVG sprites, what they are, how to use them, and what tools are available for working with them. Starting with theory, we will write a script that generates an SVG sprite step by step and conclude by discussing plugins for vite and webpack.via the TL;DR App

Developers often insert SVG directly into JSX. This is convenient to use, but it increases the JS bundle size. In the pursuit of optimization, I decided to find another way of using SVG icons without cluttering the bundle. We will talk about SVG sprites, what they are, how to use them, and what tools are available for working with them.

Starting with theory, we will write a script that generates an SVG sprite step by step and conclude by discussing plugins for vite and webpack.

What Is SVG Sprite?

An image sprite is a collection of images placed into a single image. In its turn, SVG Sprite is a collection of SVG content, wrapped into <symbol />, which is placed into <svg />.

For example, we have a simple SVG pen icon:

To obtain an SVG sprite, we will replace the <svg /> tag with <symbol />, and wrap it with <svg /> externally:

Now it is an SVG sprite, and we have an icon inside with id="icon-pen".

Ok, but we should figure out how to place this icon on our HTML page. We will use the <use /> tag with the href attribute, specifying the ID of the icon, and it will duplicate this element inside SVG.

Let's take a look at an example of how <use /> works:

In this example, there are two circles. The first one has a blue outline, and the second is a duplicate of the first one but with a red fill.

Let’s get back to our SVG sprite. Along with the usage of <use />, we will get this:

Here, we have a button with our pen icon.

So far, we have used an icon without its code in <button />. If we have more than one button on the page, it will not affect more than once the size of our HTML layout because all icons will come from our SVG sprite and will be reusable.

Creating SVG Sprite File

Let's move our SVG sprite into a separate file so that we don't have to clutter the index.html file. First, create a sprite.svg file and put a SVG sprite into it. The next step is to provide access to the icon using the href attribute in <use/>:

Automating SVG Sprite Creation

To save a lot of time on icon usage, let’s set up an automation for this process. To get easy access to icons and manage them as we want, they have to be separated, each in its own file.

First, we should put all icons in the same folder, for example:

Now, let’s write a script that grabs these files and combines them into a single SVG sprite.

  1. Create the generateSvgSprite.ts file in the root directory of your project.

  2. Install glob library:

    npm i -D glob
    

  3. Get an array of full paths for each icon using globSync:

  4. Now, we will iterate each file path and get file content using Node's built-in library fs:

    Great, we have the SVG code of each icon, and now we can combine them, but we should replace the svg tag inside each icon with the symbol tag and remove useless SVG attributes.

  5. We should parse our SVG code with some HTML parser library to get its DOM representation. I will use node-html-parser:

    We have parsed the SVG code and obtained the SVG element as if it were a real HTML element.

  6. Using the same parser, create an empty symbol element to move children of svgElement to symbol:

  7. After extracting children from svgElement, we should also get the id and viewBox attributes from it. As an id, let’s set the name of the icon file.

  8. Now, we have a symbol element that can be placed in an SVG sprite. So, just define the symbols variable before iterating the files, transform the symbolElement into a string, and push it into symbols:

  9. The final step is to create the SVG sprite itself. It represents a string with svg in “root” and symbols as children:

    const svgSprite = `<svg>${symbols.join('')}</svg>`;
    

    And if you are not considering using plugins, which I will talk about below, you need to put the file with the created sprite in some static folder. Most bundlers use a public folder:

    fs.writeFileSync('public/sprite.svg', svgSprite);
    

And this is it; the script is ready to use:

// generateSvgSprite.ts

import { globSync } from 'glob';
import fs from 'fs';
import { HTMLElement, parse } from 'node-html-parser';
import path from 'path';

const svgFiles = globSync('src/icons/*.svg');
const symbols: string[] = [];

svgFiles.forEach(file => {
  const code = fs.readFileSync(file, 'utf-8');
  const svgElement = parse(code).querySelector('svg') as HTMLElement;
  const symbolElement = parse('<symbol/>').querySelector('symbol') as HTMLElement;
  const fileName = path.basename(file, '.svg');

  svgElement.childNodes.forEach(child => symbolElement.appendChild(child));

  symbolElement.setAttribute('id', fileName);

  if (svgElement.attributes.viewBox) {
    symbolElement.setAttribute('viewBox', svgElement.attributes.viewBox);
  }

  symbols.push(symbolElement.toString());
});

const svgSprite = `<svg>${symbols.join('')}</svg>`;

fs.writeFileSync('public/sprite.svg', svgSprite);

You can put this script in the root of your project and run it with tsx:

npx tsx generateSvgSprite.ts

Actually, I’m using tsx here because I used to write code in TypeScript everywhere, and this library allows you to execute node scripts written in TypeScript. If you want to use pure JavaScript, then you can run it with:

node generateSvgSprite.js

So, let’s sum up what the script is doing:

  • It looks into src/icons folder for any .svg files.

  • It Extracts the content of every icon and creates a symbol element from it.

  • It Wraps up all the symbols into a single <svg />.

  • It creates sprite.svg file in the public folder.

How to Change Icon Colors

Let's cover one frequent and important case: colors! We created a script where the icon goes into a sprite, but this icon can have different colors throughout the project.

We should keep in mind that not only <svg/> elements can have fill or stroke attributes, but also path, circle, line, and others. There’s a very useful CSS feature that will help us - currentcolor.

This keyword represents the value of an element's color property. For example, if we use the color: red on an element that has a background: currentcolor, then this element will have a red background.

Basically, we need to change every stroke or fill attribute value to the currentcolor. I hope you are not seeing it done manually, heh. And even writing some code that will replace or parse SVG strings is not very efficient compared to a very useful tool svgo.

This is an SVG optimizer that can help not only with colors but also with removing redundant information from SVG.

Let’s install svgo:

npm i -D svgo

svgo has built-in plugins, and one of them is convertColors, which has the property currentColor: true. If we use this SVG output, it will replace colors with currentcolor. Here is the usage of svgo along with convertColors:

import { optimize } from 'svgo';

const output = optimize(
	'<svg viewBox="0 0 24 24"><path fill="#000" d="m15 5 4 4" /></svg>',
	{
	  plugins: [
			{
	      name: 'convertColors',
	      params: {
	        currentColor: true,
	      },
	    }
		],
	}
)

console.log(output);

And the output will be:

<svg viewBox="0 0 24 24"><path fill="currentColor" d="m15 5 4 4"/></svg>

Let’s add svgo into our magical script which we wrote in the previous part:

// generateSvgSprite.ts

import { globSync } from 'glob';
import fs from 'fs';
import { HTMLElement, parse } from 'node-html-parser';
import path from 'path';
import { Config as SVGOConfig, optimize } from 'svgo'; // import `optimize` function

const svgoConfig: SVGOConfig = {
  plugins: [
    {
      name: 'convertColors',
      params: {
        currentColor: true,
      },
    }
  ],
};

const svgFiles = globSync('src/icons/*.svg');
const symbols: string[] = [];

svgFiles.forEach(file => {
  const code = fs.readFileSync(file, 'utf-8');
  const result = optimize(code, svgoConfig).data; // here goes `svgo` magic with optimization
  const svgElement = parse(result).querySelector('svg') as HTMLElement;
  const symbolElement = parse('<symbol/>').querySelector('symbol') as HTMLElement;
  const fileName = path.basename(file, '.svg');

  svgElement.childNodes.forEach(child => symbolElement.appendChild(child));

  symbolElement.setAttribute('id', fileName);

  if (svgElement.attributes.viewBox) {
    symbolElement.setAttribute('viewBox', svgElement.attributes.viewBox);
  }

  symbols.push(symbolElement.toString());
});

const svgSprite = `<svg xmlns="http://www.w3.org/2000/svg">${symbols.join('')}</svg>`;

fs.writeFileSync('public/sprite.svg', svgSprite);

And run the script:

npx tsx generateSvgSprite.ts

As a result, SVG sprite will contain icons with currentColor. And these icons can be used everywhere in the project with any color you want.

Plugins

We have a script, and we can run it whenever we want, but it is slightly inconvenient that we should use it manually. So, I recommend a few plugins that can watch our .svg files and generate SVG sprites on the go:

  1. vite-plugin-svg-spritemap (for vite users)

    This is my plugin which contains basically this script that we just created in this article. The plugin has currentColor replacement enabled by default, so you can set up the plugin pretty easily.

    // vite.config.ts
    
    import svgSpritemap from 'vite-plugin-svg-spritemap';
    
    export default defineConfig({
      plugins: [
        svgSpritemap({
          pattern: 'src/icons/*.svg',
          filename: 'sprite.svg',
        }),
      ],
    });
    

  2. svg-spritemap-webpack-plugin (for webpack users)

    I used this Webpack plugin until I switched to Vite. But this plugin is still a good solution if you are using Webpack. You should manually enable color conversion, and it will look like this:

    // webpack.config.js
    
    const SVGSpritemapPlugin = require('svg-spritemap-webpack-plugin');
    
    module.exports = {
      plugins: [
        new SVGSpritemapPlugin('src/icons/*.svg', {
          output: {
            svgo: {
              plugins: [
                {
                  name: 'convertColors',
                  params: {
                    currentColor: true,
                  },
                },
              ],
            },
            filename: 'sprite.svg',
          },
        }),
      ],
    }
    

Usage in Layout

I will provide an example in React, but you can implement it where you want because it is mostly about HTML. So, as we have sprite.svg in our build folder, we can access the sprite file and create the basic Icon component:

const Icon: FC<{ name: string }> = ({ name }) => (
  <svg>
    <use href={`/sprite.svg#${name}`} />
  </svg>
);

const App = () => {
  return <Icon name="pen" />;
};

The Final Result

So, summarizing everything, to prevent a lot of manual work with icons, we:

  • can easily save and keep organized icons in the project with desirable names

  • have a script that combines all icons into a single sprite in a separate file that reduces bundle size and allows us to use these icons anywhere in the project

  • have a useful tool that helps us keep icons decluttered from unnecessary attributes and change colors in the place of usage

  • have a plugin that can watch our icon files and generate sprites on the go as a part of the build process

  • have an Icon component that is a cherry on top

Conclusion

Efficiency in development isn't just about saving time; it's about unlocking our creative potential. Automating the nitty-gritty tasks like managing icons isn't just a shortcut; it's a gateway to a smoother, more impactful coding experience. And saving time on such routine stuff, you can focus on more complex tasks and grow as a developer faster.


Written by gmakarov | Senior Software Engineer | Frontend | React | TypeScript
Published by HackerNoon on 2023/12/23