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:
<img>
tag, inline SVG can be styled and colored using CSS.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.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();
});
},
};
}
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>
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" />
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.