@svgr/core
SVGR turns an SVG file into a React component. @svgr/core is the engine underneath the tools most people actually use: it exposes a single transform(code, config, state) function that takes SVG source as a string and returns component source as a string. Core by itself is only a pipeline runner and a config loader. The work happens in plugins you register: @svgr/plugin-svgo optimizes the markup, @svgr/plugin-jsx parses the SVG, converts it to a Babel AST, and prints a React component, and @svgr/plugin-prettier formats the result. Config controls what the component looks like: whether it forwards a ref, accepts a title prop, spreads props onto the svg element, keeps or drops the width and height attributes, emits TypeScript, uses the classic or automatic JSX runtime, and whether the export is default or named. It runs at build time and outputs source code, not runtime components.
Still the standard way to get React components out of SVG files, and the generated output is good. The project has been quiet since August 2023 with pinned SVGO 3 and Prettier 2 dependencies, so use it through a bundler plugin or generate your icons once and commit them, rather than building anything new directly on core.
Use it if
- You are writing a build step, a codemod, or an icon-generation script and need SVG-to-component conversion as a function call rather than as a bundler plugin
- You want the generated component shaped a specific way: a forwarded ref, a title prop for accessibility, currentColor substituted for a hard-coded fill, or your own template that emits something other than a plain function component
- You are generating an icon library ahead of time and want the components committed as real files that people can read, diff, and tree-shake, instead of transformed invisibly by a loader
- You need the transform in a non-bundler context: a CLI, a Storybook addon, a design-token pipeline, or a server that turns uploaded SVGs into components
- You are using a bundler: @svgr/webpack, vite-plugin-svgr, or @svgr/cli already wire the plugins together and are what almost all of the download count represents. Installing core directly is the unusual path
- You expected core alone to do something: the default plugin list is empty, so transform() with no plugins configured hands your SVG string straight back unchanged. You need @svgr/plugin-jsx installed and named in config.plugins before any JSX appears
- You need maintained software: 8.1.0 shipped in August 2023 and there has been no release since. The main branch has had documentation-only commits since 2024, the newest being October 2025, and there are 122 open issues (147 including PRs)
- You are on current tooling: @svgr/plugin-svgo pins svgo ^3.0.2 while SVGO is on 4.x, and @svgr/plugin-prettier pins prettier ^2.8.7, so a project on Prettier 3 ends up with a second copy of Prettier 2 in node_modules
- You care about build time: every SVG goes through svg-parser, a HAST to Babel AST conversion, a full @babel/core transform, and optionally SVGO and Prettier. On an icon set of a few hundred files that is seconds per build, which is why generating once and committing beats transforming on every rebuild
- You are in an ESM-only or edge runtime: the package is CommonJS, and string plugin names are resolved with require(), so a plugin published as ESM only cannot be loaded by name
Setup reality
npm install @svgr/core is not enough on its own, and that is the single thing that catches everyone. Core ships an empty default plugin list, so you also install @svgr/plugin-jsx (and usually @svgr/plugin-svgo and @svgr/plugin-prettier) and list them in config.plugins by module name. The package is CommonJS with Node 14 or newer, and it drags in @babel/core plus cosmiconfig, so it is a heavier dependency than the job suggests. Two defaults will surprise you. runtimeConfig defaults to true, which means that whenever you pass state.filePath, cosmiconfig walks up the directory tree looking for .svgrrc, svgr.config.js, or an svgr key in package.json, and quietly merges whatever it finds with the config you passed; set runtimeConfig: false in a script if you want deterministic output. And the SVGO plugin only disables removeViewBox when icon is set or dimensions is false, so with default settings the viewBox attribute is stripped and your icons stop scaling. The transform function is async and returns a string of source code, so writing it to disk and formatting it are your job unless you register the Prettier plugin.
Patterns
Turn an SVG string into a componentbasic-transform
import { transform } from '@svgr/core'
import fs from 'node:fs/promises'
const svg = await fs.readFile('icons/check.svg', 'utf8')
const code = await transform(
svg,
{ plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx', '@svgr/plugin-prettier'] },
{ componentName: 'CheckIcon' },
)
await fs.writeFile('src/icons/CheckIcon.jsx', code)Plugin order is the pipeline order: optimize, then convert to JSX, then format. Drop @svgr/plugin-jsx and transform returns the SVG string untouched, because the built-in default plugin list is empty.
Use the synchronous formsync-transform
import { transform } from '@svgr/core'
const code = transform.sync(svg, {
plugins: ['@svgr/plugin-jsx'],
runtimeConfig: false,
}, { filePath: 'icons/check.svg' })transform.sync exists for loaders and scripts that cannot await. It resolves config synchronously through cosmiconfigSync, which means a config file that exports a promise or uses top-level await will fail here but work in the async form.
Emit a typed componenttypescript-output
const code = await transform(svg, {
plugins: ['@svgr/plugin-jsx'],
typescript: true,
ref: true,
memo: true,
}, { componentName: 'CheckIcon' })
// -> const CheckIcon = (props: SVGProps<SVGSVGElement>, ref: Ref<SVGSVGElement>) => ...typescript: true only changes the generated annotations; you still have to write the file with a .tsx extension yourself. ref: true wraps the component in forwardRef, and memo: true wraps that in React.memo, in that order.
Make an icon inherit the text colourcurrentcolor-icons
const code = await transform(svg, {
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
icon: true,
replaceAttrValues: { '#063855': 'currentColor', '#000': 'currentColor' },
svgProps: { focusable: 'false', 'aria-hidden': 'true' },
})replaceAttrValues matches attribute values literally, so #FFF and #ffffff are different keys and you need every spelling the designer used. icon: true swaps width and height for 1em and, as a side effect, keeps the viewBox that SVGO would otherwise remove.
Stop SVGO from removing the viewBoxkeep-viewbox
const code = await transform(svg, {
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
svgoConfig: {
plugins: [
{ name: 'preset-default', params: { overrides: { removeViewBox: false } } },
'prefixIds',
],
},
})With default settings the SVGO plugin runs preset-default, which strips viewBox when width and height are present, and the component then refuses to scale. Setting icon: true or dimensions: false disables it automatically; otherwise you override it here. Passing svgoConfig replaces the whole default, so re-add prefixIds or inlined SVGs will collide on gradient and clipPath ids.
Give the component a title propaccessible-title
const code = await transform(svg, {
plugins: ['@svgr/plugin-jsx'],
titleProp: true,
descProp: true,
ref: true,
})
// <CheckIcon title="Task complete" titleId="check-1" />titleProp adds an optional title element plus a titleId prop for the aria-labelledby wiring. It is off by default, so the standard SVGR output is a graphic with no accessible name, which is fine only if you also set aria-hidden.
Match the Create React App import stylenamed-export
const code = await transform(svg, {
plugins: ['@svgr/plugin-jsx'],
exportType: 'named',
namedExport: 'ReactComponent',
}, { componentName: 'Logo' })
// -> export { Logo as ReactComponent }
// import { ReactComponent as Logo } from './logo.svg'This is the shape people expect from Create React App and from vite-plugin-svgr's default. Switching an existing codebase between exportType 'default' and 'named' breaks every import site at once, so decide before generating.
Drop the React import for the new JSX transformautomatic-jsx-runtime
const code = await transform(svg, {
plugins: ['@svgr/plugin-jsx'],
jsxRuntime: 'automatic',
})
// preact instead:
// { jsxRuntime: 'classic-preact' }The default is 'classic', which emits import * as React from 'react' at the top of every generated file. On React 17 and newer with the automatic runtime that import is dead weight, and some lint configs flag it.
Control the generated file exactlycustom-template
const template = (variables, { tpl }) => tpl`
${variables.imports};
import { cn } from '@/lib/utils';
${variables.interfaces};
const ${variables.componentName} = (${variables.props}) => (
${variables.jsx}
);
${variables.exports};
`
const code = await transform(svg, { plugins: ['@svgr/plugin-jsx'], template })The template runs inside Babel, so variables.jsx is an AST node rather than a string and cannot be manipulated with string methods. This is the only way to add imports, wrappers, or a displayName to every generated component.
Get deterministic output in a scriptdisable-runtime-config
const code = await transform(svg, {
plugins: ['@svgr/plugin-jsx'],
runtimeConfig: false,
}, { filePath })runtimeConfig defaults to true, so whenever filePath is set, cosmiconfig searches upward for .svgrrc, svgr.config.js, or an svgr key in package.json and merges it in. In a monorepo that means the same SVG produces different components depending on which directory it lives in.
Add your own step to the pipelinecustom-plugin
import type { Plugin } from '@svgr/core'
const addBanner: Plugin = (code, config, state) =>
`// generated from ${state.filePath}. do not edit.\n${code}`
const code = await transform(svg, {
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx', addBanner],
})A plugin is just (code, config, state) returning a string, and it can be a function rather than a module name. Functions run in place; string names are loaded with require(), so ESM-only plugin packages have to be passed as functions instead.
Generate a whole icon directory oncebatch-generate-icons
import { transform } from '@svgr/core'
import fs from 'node:fs/promises'
import path from 'node:path'
const plugins = ['@svgr/plugin-svgo', '@svgr/plugin-jsx', '@svgr/plugin-prettier']
const files = await fs.readdir('svg')
for (const file of files.filter((f) => f.endsWith('.svg'))) {
const svg = await fs.readFile(path.join('svg', file), 'utf8')
const code = await transform(svg, { plugins, typescript: true, icon: true, runtimeConfig: false })
await fs.writeFile(path.join('src/icons', file.replace('.svg', '.tsx')), code)
}Omitting state.componentName means SVGR derives it from filePath, or falls back to SvgComponent when neither is given, so a whole batch can end up with identically named components. Pass componentName explicitly when you generate.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| vite-plugin-svgr | npm | You are on Vite and just want to import an SVG as a component, with SVGR wired up and cached for you |
| @svgr/webpack | npm | You are on webpack, Next.js, or Rspack and want the loader form instead of calling the transform yourself |
| unplugin-icons | npm | Your icons come from an icon set rather than from designer-supplied files, and you want on-demand components across Vite, webpack, Rollup, and esbuild from a maintained project |
| svgo | npm | You only need the SVG cleaned up and want to inline the markup yourself rather than generate a component |