Skip to main content
kld.dev

Automated SVG icon sprites: Vite Edition

I have written about using SVG sprites for icons before, but that article has been long out-of-date because it calls for a webpack loader that is no longer maintained. Plus, who uses webpack anymore?

End goals

For my latest side project, I am using SvelteKit, but this will all apply to any project that uses vite as a build tool. My end goals for the icons in this project were to:

  • Generate a sprite from all my SVG icon files. Using an SVG sprite prevents repeating the same SVG code multiple times in the HTML. Unlike an <img> tag, inline SVG can be styled and colored using CSS.
  • Keep the original SVG icons in my static assets folder, so I can still use them as images if necessary.
  • Automate all of this during the build process (and whenever I add an icon during development).

Relevant files

Here are the important files for this, as they exist in my project structure:

├── plugins
│   └── vite-plugin-icon-sprite.js
├── static
│   ├── icons
│   │   ├── heart.svg
│   │   └── star.svg
│   └── icon-sprite.svg
├── vite.config.js
  • plugins/vite-plugin-icon-sprite.js is the plugin that I wrote to generate the SVG sprite.
  • static/icons contains all of the original SVG icons.
  • static/icon-sprite.svg is the generated SVG sprite.

Custom vite plugin

I have written a custom vite plugin for my project to generate the SVG sprite, even though I’m sure there are some publicly available plugins for this. I just prefer to tailor things concisely to my needs and avoid having too many dependencies.

Here is the entire plugin with comments:

// plugins/vite-plugin-icon-sprite.js

import { promises as fs } from 'fs';
import path from 'path';

export default function IconSpritePlugin() {
  async function generateIconSprite() {
    // Read the SVG files in the static/icons folder
    const iconsDir = path.join(process.cwd(), 'static', 'icons');
    const files = await fs.readdir(iconsDir);
    let symbols = '';

    // Build up the SVG sprite from the SVG files
    for (const file of files) {
      if (!file.endsWith('.svg')) continue;
      let svgContent = await fs.readFile(path.join(iconsDir, file), 'utf8');
      const id = file.replace('.svg', '');
      svgContent = svgContent
        .replace(/id="[^"]+"/, '') // Remove any existing id
        .replace('<svg', `<symbol id="${id}"`) // Change <svg> to <symbol>
        .replace('</svg>', '</symbol>');
      symbols += svgContent + '\n';
    }

    // Write the SVG sprite to a file in the static folder
    const sprite = `<svg width="0" height="0" style="display: none">\n\n${symbols}</svg>`;
    await fs.writeFile(path.join(process.cwd(), 'static', 'icon-sprite.svg'), sprite);
  }

  return {
    name: 'icon-sprite-plugin',
    buildStart() {
      // Generate during build
      return generateIconSprite();
    },
    configureServer(server) {
      // Regenerate during development whenever an icon is added
      server.watcher.add(path.join(process.cwd(), 'static', 'icons', '*.svg'));
      server.watcher.on('change', async (changedPath) => {
        if (changedPath.endsWith('.svg')) return generateIconSprite();
      });
    },
  };
}

Updating the vite config

To use the plugin, it has to be added to the vite config:

// vite.config.js

import { defineConfig } from 'vite';
import IconSpritePlugin from './plugins/vite-plugin-icon-sprite';

export default defineConfig({
  plugins: [IconSpritePlugin()], // and any other plugins
  // ...
});

Now, static/icon-sprite.svg should be generated automatically during the build process.

The generated SVG sprite will look something like this:

<!-- static/icon-sprite.svg -->

<svg width="0" height="0" style="display: none">

<symbol id="heart" xmlns="http://www.w3.org/2000/svg" ...>
  <path ... />
</symbol>

<symbol id="star" xmlns="http://www.w3.org/2000/svg" ...>
  <path ... />
</symbol>

<!-- ... the rest of the icons -->

</svg>

Using the SVG sprite

To add an icon to a page, I now just need a little bit of markup:

<svg style="width: 1em; height: 1em; fill: currentColor">
  <use href="/icon-sprite.svg#heart"></use>
</svg>

For my SvelteKit project, I created an Icon.svelte component:

<!-- Icon.svelte -->

<script lang="ts">
	export let icon: string;
	let className = '';
	export { className as class };
</script>

<svg class={className} role="presentation" {...$$restProps}>
	<use href="/icon-sprite.svg#{icon}" />
</svg>

<style>
	svg {
		width: 1em;
		height: 1em;
		fill: currentColor;
	}
</style>

Using the Icon.svelte component is straightforward:

<Icon icon="star" />
<Icon icon="heart" class="text-lg text-red-500" />

Icon believe it’s not butter

Now I can just throw icon files into my static/icons folder willy-nilly and immediately start using them in my project. I have all the benefits of both inline SVG markup and external image files.

I look forward to writing this article yet again in a few years for the next build tool that takes over the JavaScript ecosystem.