npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

svg-sprite-kit

v1.0.0

Published

Bundles SVG icons from a folder into a single SVG sprite file. Zero dependencies, recursive scanning, id isolation, IconName types. React, Vue, Svelte and Angular — through Vite, webpack, a CLI, or Next.js without a plugin.

Readme

Bundles SVG icons from a folder into a single SVG sprite file. Zero dependencies, recursive scanning, id isolation, IconName types. React, Vue, Svelte and Angular — through Vite, webpack, a CLI, or Next.js without a plugin.

npm weekly downloads install size dependencies: 0 MIT license

Live demo — the icon grid with search, and every option shown side by side. The page is built by the package itself, from the icons in demo/icons.

Contents

Quick start

npm install -D svg-sprite-kit
yarn add -D svg-sprite-kit
pnpm add -D svg-sprite-kit
// vite.config.ts
import { defineConfig } from 'vite';
import { svgSprite } from 'svg-sprite-kit/vite';

export default defineConfig({
  plugins: [
    svgSprite({
      iconsDirectory: './src/icons',
      spriteFilePath: './public/sprite.svg'
    })
  ]
});
<svg><use href="/sprite.svg#arrow-left" /></svg>

A symbol's id is the file path relative to the icons folder: arrow-left.svg → #arrow-left, ui/nav/close.svg → #ui-nav-close.

That is a complete working config — only iconsDirectory and spriteFilePath are required. The rest — types, a demo page, symbolId, currentColor — are opt-in, and without them no extra files are ever written.

Icon in a component

The sprite is a plain file in public/, so rendering comes down to <use> inside an <svg>. A wrapper is there only to avoid repeating the path.

React

// src/components/Icon.jsx
export function Icon({ name, size = 24 }) {
  return (
    <svg width={size} height={size} fill="currentColor" aria-hidden="true">
      <use href={`/sprite.svg#${name}`} />
    </svg>
  );
}
import { Icon } from './components/Icon';

export function Toolbar() {
  return (
    <button className="icon-button">
      <Icon name="arrow-left" />
      Back
    </button>
  );
}

Color comes from ordinary CSS — .icon-button { color: #2563eb } paints the icon, because the <svg> carries fill="currentColor". That only works while the icons themselves have no hard-coded colors; if they do, turn on currentColor.

With TypeScript the same component gets the icon name checked, provided typesFilePath is set:

// src/components/Icon.tsx
import type { IconName } from '../icons';

export function Icon({ name, size = 24 }: { name: IconName; size?: number }) {
  return (
    <svg width={size} height={size} fill="currentColor" aria-hidden="true">
      <use href={`/sprite.svg#${name}`} />
    </svg>
  );
}
<Icon name="arow-left" />
// Type '"arow-left"' is not assignable to type 'IconName'. Did you mean '"arrow-left"'?

Vue 3

<!-- src/components/Icon.vue -->
<script setup>
defineProps({
  name: { type: String, required: true },
  size: { type: Number, default: 24 }
});
</script>

<template>
  <svg :width="size" :height="size" aria-hidden="true">
    <use :href="`/sprite.svg#${name}`" />
  </svg>
</template>

<style scoped>
svg {
  fill: currentColor;
}
</style>
<script setup>
import Icon from './components/Icon.vue';
</script>

<template>
  <button class="icon-button">
    <Icon name="arrow-left" />
    Back
  </button>
</template>

With TypeScript the icon name is checked at compile time just the same:

<script setup lang="ts">
import type { IconName } from '../icons';

defineProps<{ name: IconName; size?: number }>();
</script>

Svelte and SvelteKit

SvelteKit serves static files from static/, not from public/ — that is the only thing setting this config apart from the quick start. The link stays the same, /sprite.svg. In a Svelte project without Kit static files live in public/, and the quick-start config applies verbatim.

// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { svgSprite } from 'svg-sprite-kit/vite';

export default defineConfig({
  plugins: [
    sveltekit(),
    svgSprite({
      iconsDirectory: './src/icons',
      spriteFilePath: './static/sprite.svg', // not public/
      typesFilePath: './src/lib/icons.ts'
    })
  ]
});
<!-- src/lib/components/Icon.svelte -->
<script lang="ts">
  import type { SVGAttributes } from 'svelte/elements';

  import type { IconName } from '$lib/icons';

  interface Props extends SVGAttributes<SVGSVGElement> {
    name: IconName;
    size?: number;
  }

  let { name, size = 24, ...rest }: Props = $props();
</script>

<svg width={size} height={size} aria-hidden="true" {...rest}>
  <use href={`/sprite.svg#${name}`} />
</svg>

<style>
  svg {
    fill: currentColor;
  }
</style>
<script>
  import Icon from '$lib/components/Icon.svelte';
</script>

<button class="icon-button">
  <Icon name="arrow-left" />
  Back
</button>

The example uses Svelte 5 runes; on Svelte 4 the same lines read export let name; and {...$$restProps}. Extending SVGAttributes<SVGSVGElement> is what makes the rest props work: without it <Icon name="plus" class="big" /> fails type checking.

If the site lives under a subpath (config.kit.paths.base), the absolute /sprite.svg will miss the file — then import { base } from '$app/paths' and href={`${base}/sprite.svg#${name}`}.

Every icon at once

iconNames is a plain array, so a gallery is a matter of mapping over it. Handy for a design system page or a Storybook story:

import { iconNames } from '../icons';

export function IconGallery() {
  return (
    <div className="gallery">
      {iconNames.map((name) => (
        <figure key={name}>
          <Icon name={name} size={32} />
          <figcaption>{name}</figcaption>
        </figure>
      ))}
    </div>
  );
}

In plain HTML

<svg width="24" height="24" fill="currentColor" aria-hidden="true">
  <use href="/sprite.svg#arrow-left"></use>
</svg>

Why this package

| Package | Direct dependencies | Packages in the tree | On disk | | ---------------------------- | ------------------- | -------------------- | ---------- | | svg-sprite-kit | 0 | 1 | 159 KB | | vite-plugin-svg-spritemap | 5 | 24 | 8.4 MB | | svg-sprite-loader | 8 | 210 | 11.0 MB | | svg-spritemap-webpack-plugin | 9 (+4 peer) | 23 | 11.7 MB | | svg-sprite | 17 | 93 | 13.8 MB | | vite-plugin-svg-icons | 8 | 227 | 16.6 MB |

The size gap is neither an accident nor an optimization. The core imports no bundler at all, so the package has neither dependencies nor peerDependencies. The Vite and webpack adapters describe the bundler types structurally and ship as separate entry points, Next.js needs only the core, and SVG is parsed with strings instead of a DOM parser.

The second difference is reach. The other packages are tied to one bundler; here Vite, webpack, Next.js and a CLI are covered by a single install:

| | svg-sprite-kit | vite-plugin-svg-spritemap | svg-spritemap-webpack-plugin | svg-sprite | | -------------------- | --------------------- | ------------------------- | ---------------------------- | ---------- | | Vite | yes | yes | no | no | | webpack | yes | no | yes | no | | Next.js | yes, without a plugin | no | no | no | | CLI | yes | no | no | yes | | Minimum Node version | 18 | 20.19 | 20.11 | 12 |

Features

  • Recursive traversal, across several folders at once. Nesting is reflected in the id, your own and vendor icons end up in one file, and the build order is the same on every machine.
  • id and class isolation inside icons. Gradients, masks and <style> rules that share names no longer clash in the sprite's shared document — and clash they do, constantly, because Illustrator gives every icon the class .st0.
  • A typo in an icon name is caught by the compiler. The generated module exports the type IconName and the list iconNames, which is also what you map over to render a catalog.
  • A demo page with every icon. Self-contained HTML with search and click-to-copy #id — open it and see what the set actually holds.
  • Works without a bundler too. The CLI covers projects where the build config is out of reach.

CLI

npx svg-sprite-kit ./src/icons --output ./public/sprite.svg
Bundles SVG icons from a folder into a single sprite file.

Usage:
  svg-sprite-kit <folder...> --output <file> [options]

Options:
  -o, --output <file>      path to the generated sprite (required)
  -t, --types <file>       generate a module holding the IconName type
                           (an extension other than .ts/.mts — plain JS, no type)
  -d, --demo <file>        generate a demo page listing every icon
      --symbol-id <template>  symbol id template, defaults to [dir]-[name]
      --no-isolate-ids     do not prefix ids inside icons
      --no-isolate-classes do not prefix classes from <style> inside icons
      --current-color      replace hard-coded colors with currentColor
  -w, --watch              watch for changes and rebuild
  -h, --help               show this help
  -v, --version            show the version

Several folders may be given. A flag's value is written after a space or after =.

The call is rarely typed by hand — usually it hangs off an npm script, and then the sprite rebuilds itself before every start and every build:

{
  "scripts": {
    "prestart": "svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/icons.ts",
    "prebuild": "svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/icons.ts"
  }
}

The pre prefix is all it takes: npm start and npm run build invoke prestart and prebuild themselves, and the commands you type stay the same. This is how the package plugs into projects where the bundler config is out of reach — Angular, for one.

Options

The same set is accepted by svgSprite() for Vite, SvgSpritePlugin for webpack and createSpriteGenerator() for Next.js — the adapters add no fields of their own.

iconsDirectory — where icons come from

iconsDirectory: './src/img/icons'; // one folder
iconsDirectory: ['./src/img/icons', './src/vendor-icons']; // several

The traversal is recursive. An array is what you want when your own icons sit apart from vendor ones — they end up in a single file. A missing folder is not an error: a warning goes out and the build carries on.

spriteFilePath — where the sprite goes

spriteFilePath: './public/sprite.svg'; // Vite, Next.js
spriteFilePath: './static/sprite.svg'; // SvelteKit

Folders along the path are created for you. The file has to land in the static directory, or the browser will never get it: /sprite.svg resolves from the server root.

symbolId — naming the symbols

[name] is the file name without .svg, [dir] is the subfolder path with separators turned into hyphens.

| File | '[dir]-[name]' (default) | 'icon-[name]' | | ------------------ | -------------------------- | --------------- | | arrow.svg | #arrow | #icon-arrow | | ui/nav/close.svg | #ui-nav-close | #icon-close |

For files at the root [dir] is empty and the dangling hyphen is trimmed. When two files produce the same id, the first one in traversal order wins and the rest are skipped with a warning. That order depends neither on the filesystem nor on when files were created: folders come in the order listed, files precede subfolders, and subfolders go alphabetically.

isolateIds — fixing id clashes inside icons

isolateIds: true; // default

Every icon lives in one shared document, so identical gradient and mask ids collide: the browser applies the first one to both icons. Internal ids get prefixed with the symbol name, and references to them are rewritten to match:

before: <linearGradient id="a"/><path fill="url(#a)"/>
after:  <linearGradient id="sun-a"/><path fill="url(#sun-a)"/>

References to ids that the icon does not declare itself are left alone. Setting false makes sense only if something outside points at those internal ids.

isolateClasses — the same for classes from <style>

isolateClasses: true; // default

The same trouble, only sneakier: CSS inside a <symbol> is not scoped to that symbol and applies to the whole sprite document. Illustrator names every icon's class .st0, so without a prefix the last rule in the file wins:

before: <symbol id="red"><style>.st0{fill:#E00000}</style><circle class="st0"/></symbol>
        <symbol id="blue"><style>.st0{fill:#0000E0}</style><rect class="st0"/></symbol>
        both icons end up in the color of the last one

after:  <symbol id="red"><style>.red-st0{fill:#E00000}</style><circle class="red-st0"/></symbol>
        <symbol id="blue"><style>.blue-st0{fill:#0000E0}</style><rect class="blue-st0"/></symbol>

Only classes with a matching rule in the icon's own <style> get renamed. A class without a rule is a hook for the page's stylesheet and must stay untouched:

<path class="st0 icon-arrow"/> → <path class="menu-st0 icon-arrow"/>

currentColor — inheriting the text color

currentColor: false; // default
before: <path fill="#333333"/><path fill="none" stroke="#000"/>
after:  <path fill="currentColor"/><path fill="none" stroke="currentColor"/>
.button:hover .icon {
  color: red;
}

Off by default, because such a replacement destroys multi-color icons. What survives untouched: fill="none" and stroke="none" (otherwise outlined icons would be filled solid), url(#grad) references (otherwise gradients would vanish) and stop-color (otherwise a gradient would collapse into one color). Colors in a style attribute and inside a <style> block are handled alongside attributes.

typesFilePath — a module with icon names

typesFilePath: './src/icons.ts';
// src/icons.ts
// Generated by svg-sprite-kit. Do not edit by hand.

export const iconNames = ['arrow-left', 'ui-nav-close'] as const;

export type IconName = (typeof iconNames)[number];

Gives autocompletion and catches a mistyped icon name at compile time — the React section shows the check in action. Without the option no file is written.

In a project without TypeScript use the .js extension: you get a plain ESM module with no type but the same list of names.

typesFilePath: './src/icons.js';
// src/icons.js
export const iconNames = ['arrow-left', 'ui-nav-close'];

The format follows the extension: .ts and .mts get TypeScript, everything else plain JS. The module is always ESM, so webpack and Vite pick up the export in a .js file regardless of the type field in package.json. If Node.js itself reads the file, reach for .mjs instead: without "type": "module" it treats .js as CommonJS and trips over export.

demoFilePath — a page listing every icon

demoFilePath: './public/icons.html';

Self-contained HTML: a grid of icons, search by name, click to copy the #id. It pulls in no external resources. The link to the sprite is resolved relative to the page itself, so keep the two side by side. Opens at an address like http://localhost:5173/icons.html.

watch — whether to watch for changes

watch: true; // default

In the Vite and webpack adapters it cancels the subscription to the bundler's watcher; in the core it turns generator.watch() into a no-op. Watching is done by the bundler's own watcher rather than by this package, which is why server.watch and watchOptions are never ignored. For Vite and webpack keep the default: a production build sets up no subscription anyway.

root — base for relative paths

root: process.cwd(); // default

iconsDirectory, spriteFilePath, typesFilePath and demoFilePath are resolved against it. Worth touching only when the build does not run from the project root — in a monorepo, say.

onWarn — where warnings go

onWarn: (message) => myLogger.warn(message);

Defaults to console.warn with the [svg-sprite-kit] prefix; the adapters route messages into the Vite or webpack logger instead. Warnings come from a missing folder, a file without a root <svg> and an id collision.

The whole configuration

svgSprite({
  iconsDirectory: ['./src/img/icons'], // a one-element array is just a string
  spriteFilePath: './public/sprite.svg',
  typesFilePath: './src/icons.ts',
  demoFilePath: './public/icons.html',
  symbolId: '[dir]-[name]', // the default, can be omitted
  isolateIds: true, // the default, can be omitted
  watch: true // the default, can be omitted
});

And the same plugin with everything optional stripped away:

svgSprite({
  iconsDirectory: './src/img/icons',
  spriteFilePath: './public/sprite.svg'
});

Vite

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { svgSprite } from 'svg-sprite-kit/vite';

export default defineConfig({
  plugins: [
    react(),
    svgSprite({
      iconsDirectory: './src/img/icons',
      spriteFilePath: './public/sprite.svg',
      typesFilePath: './src/icons.ts',
      demoFilePath: './public/icons.html'
    })
  ]
});

This is the extended form: typesFilePath and demoFilePath are optional. The plugin does not care about the framework — see Vue and Svelte under "Icon in a component".

The sprite is built on buildStart. In dev mode the plugin subscribes to Vite's own watcher and sends full-reload after an icon changes — but only when the sprite's content actually changed.

webpack

// webpack.config.js
import { SvgSpritePlugin } from 'svg-sprite-kit/webpack';

export default {
  plugins: [
    new SvgSpritePlugin({
      iconsDirectory: './src/icons',
      spriteFilePath: './public/sprite.svg'
    })
  ]
};

The sprite is built on beforeCompile. Icon folders are registered in contextDependencies, so in watch mode webpack's own watcher tracks them — the plugin sets up no watcher of its own and does not ignore watchOptions.

Next.js

No plugin needed: the config file runs before both next dev and next build, under either bundler — webpack and Turbopack alike.

// next.config.mjs
import { createSpriteGenerator } from 'svg-sprite-kit/generator';

const sprite = createSpriteGenerator({
  iconsDirectory: './icons',
  spriteFilePath: './public/sprite.svg',
  currentColor: true,
  watch: true
});

sprite.generate();
sprite.watch();

export default {};

The options are the same as the adapters' — one object fits all three. watch: false turns the sprite.watch() call into a no-op, so the mode is switched in the options rather than around the call. currentColor: true is here for the component from the React section; on a multi-color icon set you would drop that option instead.

Calling watch() does not get in the way of next build even if you leave watch: true: the watcher is marked unref() and does not keep the process alive. The option saves a file descriptor, nothing more.

Unlike Vite, the page will not reload by itself: a Next config has no channel to the dev server. watch() keeps the sprite file current, but the tab has to be refreshed by hand.

Angular

There is no plugin: the sprite is built through the CLI from npm scripts, which makes the setup independent of the Angular version and of the builder.

{
  "scripts": {
    "prestart": "svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/app/icons.ts --current-color",
    "prebuild": "svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/app/icons.ts --current-color"
  }
}

npm runs prestart and prebuild itself, right before ng serve and ng build — the commands you type stay the same. The static directory is public/ on Angular 18 and newer and src/assets/ before that; the -o path moves with it. The --current-color flag is there for the fill: currentColor in the component below — what it actually does.

// src/app/icon.component.ts
import { Component, input } from '@angular/core';

import type { IconName } from './icons';

@Component({
  selector: 'app-icon',
  template: `
    <svg
      [attr.width]="size()"
      [attr.height]="size()"
      aria-hidden="true"
      focusable="false"
    >
      <use [attr.href]="'/sprite.svg#' + name()" />
    </svg>
  `,
  styles: `
    svg {
      fill: currentColor;
    }
  `
})
export class IconComponent {
  readonly name = input.required<IconName>();
  readonly size = input(24);
}
<button class="icon-button">
  <app-icon name="arrow-left" />
  Back
</button>

Three things Angular will remind you about on its own:

  • [attr.href], not href="{{ ... }}". SVG attributes have no matching DOM properties, so plain interpolation does nothing here — the binding has to go through attr..
  • focusable="false". Without it the icon catches focus during tab navigation.
  • strictTemplates in tsconfig.json. Icon names are only checked in templates with it on; otherwise input.required<IconName>() has no effect.

To keep icons up to date while you work, run a second process alongside ng serve:

npx svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/app/icons.ts --current-color --watch

The sprite will stay current, but the tab has to be refreshed by hand — a separate process has no channel to the dev server, same as the Next.js config.

Core API

import { createSpriteGenerator } from 'svg-sprite-kit';

createSpriteGenerator(options)

| Option | Type | Default | Description | | ---------------- | ----------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- | | iconsDirectory | string \| readonly string[] | — | Folder holding the source icons, or several folders | | spriteFilePath | string | — | Path to the generated sprite | | root | string | process.cwd() | Base for resolving relative paths | | symbolId | string | '[dir]-[name]' | Symbol id template: [dir] is the subfolder path, [name] the file name | | isolateIds | boolean | true | Whether to prefix ids inside icons with the symbol name | | isolateClasses | boolean | true | Whether to prefix classes from <style> inside icons | | currentColor | boolean | false | Whether to replace hard-coded colors with currentColor | | typesFilePath | string | — | Path to the generated module with IconName. An extension other than .ts/.mts yields plain JS without the type | | demoFilePath | string | — | Path to the generated demo page | | watch | boolean | true | Whether watching the icons is allowed | | onWarn | (message: string) => void | console.warn | Where warnings go |

Each option is covered with examples under Options. The same set is accepted by svgSprite() from svg-sprite-kit/vite and SvgSpritePlugin from svg-sprite-kit/webpack — the adapters have no options type of their own.

Returns an object:

| Member | Description | | ------------------------------ | ----------------------------------------------------------------------------------- | | generate(): boolean | Builds and writes the sprite, types and demo. true when at least one file changed | | watch(onChange?): () => void | Watches the folders. Returns a stop function | | isIconFile(path): boolean | Whether the path belongs to a source icon | | iconsFolders | Absolute paths to the icon folders | | spriteFile | Absolute path to the sprite file |

generate() writes a file only when its content differs from what is already there. The guard against endless regeneration rests on that: a watcher event that leaves the sprite unchanged triggers no reload and spawns no follow-up event.

Repeated watch() calls with the same set of paths share one set of watchers — which matters for Next.js, as it loads the config several times within a process. Every call returns its own close(); the watchers shut down once the last one is closed.

A missing icons folder is not an error: generate() and watch() emit a warning and return.

Limitations

  • SVG parsing is string-based, without a DOM parser. A nested <svg> inside an icon will not be handled correctly. That is the price of zero dependencies.
  • Not every attribute of the root <svg> survives. viewBox and the presentation attributes move onto the <symbol> — fill, stroke, the stroke-* settings, opacity, color, fill-rule, clip-rule, vector-effect — so icon sets that keep their styling on the root tag, Lucide and Feather among them, come out looking as drawn. Dropped on purpose: width and height (on a symbol they define its viewport and would break scaling through <use>), class and style (they drag the page's stylesheet into the sprite), and id.
  • SVGO is not built in. Optimize icons as a separate step — why.
  • Symbol id collisions. Two files with the same symbolId cannot coexist: the first one in traversal order stays, the rest are skipped with a warning.

Installation and Node versions

Node.js 18 or newer is required.

The package ships as ESM only. On Node.js 20.19+ and 22.12+ it can also be pulled in through require() — those versions load ESM synchronously. On Node.js 18 require() will not work: the config importing the package has to be a module. vite.config.ts (Vite loads it as ESM) and next.config.mjs qualify; next.config.js with require() does not.

Types for Vite and webpack

The adapters describe the bundler types themselves so that neither bundler has to be a dependency. Normally they assign to the real types without a single cast. If a bundler version drifts away from them, a targeted cast helps:

plugins: [svgSprite({ ... }) as unknown as PluginOption];

FAQ

I only use Vite. Do I ship the webpack adapter along with it?

Only as 8.8 KB sitting on disk — 5% of the installed package. Nothing else: exports declares separate entry points, and ESM loads a file when something imports it, so dist/webpack.js is never read, never type-checked, and never bundled. The adapter does not drag in webpack either — the bundler types are declared structurally, which is why this package has no dependencies and no peerDependencies at all.

Does any of this end up in my client bundle?

No. The package runs in Node at build time and writes files. The only thing a browser downloads is the sprite itself, as a plain static file.

Why does the whole page reload when I edit an icon, instead of hot-swapping it?

The sprite is a static file, not a module in the bundler's graph, so there is nothing to hot-swap: the plugin sends full-reload instead, and only when the sprite's content actually changed.

Can I inline the sprite into the DOM instead of fetching it?

Not through an option, but nothing stops you from doing it by hand — the sprite is an ordinary file. In Vite, import sprite from '../static/sprite.svg?raw', inject it once in the root layout, and reference symbols as href="#arrow-left" without a path. You trade one request for a sprite that is embedded in the HTML and no longer cached on its own; on a large icon set that is usually the worse deal.

<img src="/sprite.svg#arrow-left"> shows nothing. Why?

<symbol> is reachable only through <use> inside an SVG document. <img> and CSS url() need a fragment that defines a viewport — a <view> element — and this package does not generate one. Use <use>, or embed the icon as its own file.

My icons ignore the CSS color around them.

Their colors are still hard-coded in the source files. Turn on currentColor — with the caveat that it flattens multi-color icons into a single color.

Why is SVGO not built in?

Because it would end the package's zero-dependency promise: SVGO and its own dependencies weigh more than everything shipped here. Optimizing icons is also a separate concern with its own schedule — it belongs to the moment icons enter the repository, not to every rebuild. Run it as its own step, for example npx svgo --folder ./src/icons, and this package will bundle whatever it finds.

Does it work with Svelte, Angular, Astro, Nuxt, …?

Anything built on Vite or webpack — the adapters know nothing about frameworks and only need a place in the plugin list. Where the bundler config is out of reach, an npm script running the CLI covers it; see Angular.