Vite + React setup
Set svgworm up in a Vite and React app in three steps. You end up with:
- a hashed sprite in
public/icons/; - typed icon names;
- an
Iconcomponent.
Vite serves public/ as it is and copies it into dist/, so the sprite needs no plugin.
Requires Node ^22.20 || >=24.11 and React 19 (@svgworm/react has react ^19 as a peer). The working example uses Vite 8.
A Vite app renders on the client, so Icon runs in the browser. The sprite is a separate cached file and is not JavaScript. But the manifest ships in your bundle: every icon’s id, viewBox and delivery, about 200 bytes per icon before compression. Keep the source folder to the icons you use. For zero client JS, use Next.js Server Components or Astro.
Steps#
-
Install and set up:
pnpm add -D svgworm && pnpm add @svgworm/react && pnpm svgworm init # npm: npm install -D svgworm && npm install @svgworm/react && npx svgworm init # yarn: yarn add -D svgworm && yarn add @svgworm/react && yarn svgworm initinitdoes the setup for you:- Config: writes
svgworm.config.json. - Scripts: adds
predevandprebuildscripts that runsvgworm. - Component: creates
components/icon.tsx(src/components/icon.tsxwhensrc/exists). - First build: if
icons/is missing or empty, it adds a sampleicons/svgworm.svg. Then it builds once.
It does not detect Vite, so it does not read
vite.config.*. SetpublicPathyourself when you use abase. See the CLI reference for every flag. - Config: writes
-
Put your SVG files in
icons/(delete the sample) and build:pnpm svgwormpnpm devandpnpm buildrun it too, through thepredevandprebuildscripts. It writes the sprite topublic/icons/and the manifest and types tosvgworm/:File Contents public/icons/icons.<hash>.svgSprite (plus icons.stack.<hash>.svgwhen an icon needs it)svgworm/icons.jsManifest (data only) svgworm/icons.d.tsTypes: IconNameand the manifest shapesvgworm/manifest.jsonSame manifest, for scripts and templates -
Use it in any component:
import { Icon } from "./components/icon"; export const Nav = () => ( <nav> <Icon name="home" /> <Icon name="brand/sunset" width={32} height={32} /> <Icon name="home" title="Go home" /> </nav> );- Names come from the file paths:
homeisicons/home.svgandbrand/sunsetisicons/brand/sunset.svg, lower-cased with-between words. - Decorative by default: the icon is
aria-hiddenunless you passtitleorlabel. - Type-checked: an unknown
nameis a type error, so an editor lists every valid name. - No
'use client'rule: every component runs in the browser, so importingIconanywhere is fine.
- Names come from the file paths:
Set up by hand#
Without init, do its part yourself after installing the two packages:
-
Build the sprite from your icon folder:
pnpm svgwormWith no config file, svgworm reads the
icons/folder. For another folder, pass--source <dir>on every run, or createsvgworm.config.json(see Config). -
Create the component once:
// src/icon.tsx import { createIcon } from "@svgworm/react"; import { manifest } from "../svgworm/icons.js"; export const Icon = createIcon(manifest); -
Run the CLI before
devandbuild:{ "scripts": { "predev": "svgworm", "prebuild": "svgworm" } }Yarn 2+ never runs
prescripts. With it, put the command in front of the scripts instead:"dev": "svgworm && vite"and"build": "svgworm && vite build".
Cache for a year#
Vite copies public/ as it is and has no header setting for it. The file names are content-hashed, so a long cache is safe, and your host sets it. vite preview and the dev server send no long cache.
-
Cloudflare and Netlify, in
public/_headers:/icons/* Cache-Control: public, max-age=31536000, immutable X-Content-Type-Options: nosniff -
Vercel, in
vercel.json:{ "headers": [ { "source": "/icons/(.*)", "headers": [ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }, { "key": "X-Content-Type-Options", "value": "nosniff" } ] } ] } -
Anything else: give
/icons/*the same two headers in nginx, Apache, S3 and CloudFront, or your own server. With abase, the rule matches<base>icons/*.
base#
Why: Vite copies public/icons/ to dist/icons/, and your host serves dist/ under /app/. So the sprite lives at /app/icons/…. Without publicPath, the manifest still points at /icons/… and every icon 404s. outDir stays public/icons. Only the URL changes, not where the file is written.
{
"scripts": {
"prebuild": "svgworm --public-path /app/icons",
"build": "vite build --base /app/"
}
}
An absolute base (https://cdn.example.com/app/) serves public/ from that origin, so the sprite is cross-origin. You have two options:
- Proxy: proxy
/icons/*through the page’s origin to keep zero JS. - Loader: build with
--cross-origin inject --public-path https://cdn.example.com/app/iconsand render the loader (Sprites on a CDN).
If the base differs per environment, keep it in one place and pass it to both. The example’s check:base script builds with base: '/app/' and checks that the bundle, the manifest and dist/icons/ agree.
What to commit#
Two choices. Pick one and stick with it:
- Commit
public/icons/andsvgworm/and runsvgworm checkin CI. Every checkout type-checks without a build step. Reviews show icon changes as diffs of the sprite and the manifest. CI fails when someone edits an icon and forgets to rebuild. - Ignore both folders (
public/icons/andsvgworm/in.gitignore) and rely on thepredevandprebuildscripts. Less noise in reviews. But anything that runs beforevite build, such astscor ESLint in CI, must runsvgwormfirst. Otherwise it will not findsvgworm/icons.d.ts.
CI#
Run svgworm check before the build. It compiles the icons and compares the result with the committed output. CI fails (exit 1) when someone changed an icon and forgot to rebuild:
pnpm svgworm check
pnpm build
Working example#
examples/vite-react is a complete Vite 8 and React app with these steps. CI runs svgworm check and builds it. Then it checks three things:
- Bundle: it references the hashed sprite from the manifest.
- Sprite: it is in
dist/icons/. - Output:
dist/icons/holds only the manifest’s files.
A second build under base: '/app/' runs the same checks.
Next steps#
- Icon component: every prop, the markup per delivery, sizing and coloring with CSS.
- How it works: the sprite, the stack, delivery forms, the manifest and determinism.
- CLI reference: every command, flag and exit code.
- Config:
svgworm.config.jsonand per-icon color overrides. - Troubleshooting: blank icons, 404s, wrong colors, collisions,
checkin CI. - Using Next.js? Read the Next.js setup. Using Astro? Read the Astro setup.