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.
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.
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/>
:
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.
Create the generateSvgSprite.ts
file in the root directory of your project.
Install glob library:
npm i -D glob
Get an array of full paths for each icon using globSync
:
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.
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.
Using the same parser, create an empty symbol
element to move children of svgElement
to symbol
:
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.
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
:
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:
src/icons
folder for any .svg
files.
<svg />.
sprite.svg
file in the public
folder.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.
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:
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',
}),
],
});
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',
},
}),
],
}
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" />;
};
So, summarizing everything, to prevent a lot of manual work with icons, we:
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.