Migrate from SVGR
SVGR turns each SVG into a React component, so every icon ships as JavaScript and its path data is repeated in the HTML on every render. svgworm puts the icons into one cacheable sprite and renders <Icon name="logo" />. The name is type-checked.
The switch has three steps:
- Set svgworm up on the folder that already holds your SVGs.
- Run the codemod. It rewrites the imports and the JSX.
- Remove the SVGR config once no import is left.
Both can run side by side while you migrate.
1. Set svgworm up#
Point init at the folder with your SVG files:
npx svgworm init --source src/assets/icons --yes
pnpm add @svgworm/react
It writes svgworm.config.json, the predev/prebuild scripts and components/icon.tsx (or src/components/icon.tsx), then builds once. The Next.js and Vite pages cover the details.
2. Run the codemod#
npx svgworm migrate svgr # dry run: prints every change, writes nothing
npx svgworm migrate svgr --write # applies them
It reads the same svgworm.config.json. It scans every .js, .jsx, .ts and .tsx file in the project for .svg imports. It skips node_modules, dot folders, svgworm’s own output, and dist, build, out and coverage at the project root. Each import is matched to the icon svgworm builds from that file:
| Before | After |
|---|---|
import Logo from './logo.svg' (Next.js, @svgr/webpack) |
import { Icon } from '../components/icon' |
import { ReactComponent as Logo } from './logo.svg' (Create React App) |
the same |
import Logo from './logo.svg?react' (vite-plugin-svgr) |
the same |
import Logo from '@/icons/logo.svg' (tsconfig paths) |
the same |
<Logo className="h-6" aria-label="Home" /> |
<Icon name="logo" className="h-6" aria-label="Home" /> |
<ArrowLeft></ArrowLeft> (icons/arrows/left.svg) |
<Icon name="arrows/left" /> |
- Props carry over.
className,style,width,height,aria-*andtitlecarry over. TheIcontakes the same SVG props, and a wrong one is a type error. Spread props ({...props}) carry over too, but TypeScript does not check the keys inside a spread. Check that one does not pass aref, children,fillorstroke. - Imports. It adds one
Iconimport per file and removes each.svgimport it fully converted. A URL import (import logoUrl from './logo.svg'used as<img src={logoUrl}>) is not SVGR, so it stays. - Formatting. It only replaces text, so your formatting is kept. Run your formatter on the changed files afterwards.
- Re-running. Running it again is safe. It finds nothing left to convert.
It needs TypeScript 5 or 6 to read your code. A project that already has it needs nothing more. Otherwise run npx -p typescript@6 -p svgworm svgworm migrate svgr. TypeScript 7 has no JS compiler API yet.
What it leaves on SVGR#
An import it cannot convert safely is listed with file:line and a reason. That file is not touched, and the command exits 1, so CI or an agent can tell that the migration is not finished.
skip src/Tooltip.tsx:55 InfoSvg from "./assets/svg/info.svg" is used as a value, not as a JSX tag; rewrite it by hand with <Icon name="info" />
skip src/Search.tsx:67 SearchSvg from "./assets/svg/search.svg" sets fill, which SVGR applies to the inline paths; color a sprite icon with color or CSS (currentColor)
| Reason | What to do |
|---|---|
Used as a value (styled(Logo), icons = { logo: Logo }, component={Logo}) |
Rewrite it with the <Icon name> the message gives. For styled(Logo), style a wrapper or pass className |
fill or stroke prop |
Set color (or CSS color) instead. Single-color icons draw with currentColor |
ref, children, titleId, desc |
The Icon takes none of them. Use title or label for an accessible name |
Animated icon (SMIL or @keyframes in the file) |
Keep SVGR for it (see below) |
| Outside the configured sources | Add its folder to sources, or keep SVGR for it |
| Not a valid icon, or a name collision | Fix the file. svgworm prints the same error |
The file already declares Icon (another import, a type, a prop) |
Rename the other Icon, then run again |
Re-exported from a barrel (export { default as Logo } from './logo.svg') |
Import the icon where it is used, or render <Icon name> there instead |
3. Remove SVGR#
Remove SVGR only after npx svgworm migrate svgr exits 0. Until then, the imports it left still need the loader. The codemod does not see require('./x.svg'), import('./x.svg') or export * from './x.svg', so search for .svg' in your code before you delete the loader.
Next.js: delete the .svg rule from next.config.*, for both Turbopack and webpack, and then remove @svgr/webpack:
// next.config.ts: delete these
const nextConfig = {
turbopack: {
rules: { "*.svg": { loaders: ["@svgr/webpack"], as: "*.js" } },
},
webpack(config) {
config.module.rules.push({ test: /\.svg$/i, use: ["@svgr/webpack"] });
return config;
},
};
Vite: remove svgr() from vite.config.*, remove /// <reference types="vite-plugin-svgr/client" />, and uninstall vite-plugin-svgr.
Create React App: SVGR is built in, so there is nothing to remove.
Then delete any declare module '*.svg' file you wrote for SVGR. Icon names are now typed by svgworm’s generated svgworm/icons.d.ts.
The limit: inner paths#
svgworm renders each icon as <svg><use href="sprite.svg#id"/></svg>. The browser draws the paths inside a <use> shadow tree, and the page’s CSS and JavaScript cannot reach them. You can color the whole icon (color, currentColor), size it and label it. You cannot style or animate one path, for example:
- a CSS rule like
.logo path:nth-child(2) { … }; - a Framer Motion path animation;
- a
@keyframesrule inside the SVG.
svgworm has no inline-sprite mode to work around this (ADR-0024), so keep SVGR for those icons. Both work in the same app. To leave such a file out of the sprite, add it to the source’s exclude globs in the config.