mrkeyoor.com_
Thu 06 Aug 08:49 UTC
npmWeb Frontendupdated 06 Aug 2026

@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.

Verdict

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.

API stability5/5transform(code, config, state) and the config keys have not changed since 8.0.0 in May 2023, and nothing has been released since to change them. Total stability here is a consequence of the project being idle, not of deliberate restraint
Docs4/5react-svgr.com documents every config option, has separate pages for the CLI, webpack, Node API, and Next.js, and the live playground shows what any option combination produces. The gap is the Node API page, which does not make it obvious that core with no plugins is a no-op
Maintenance2/5No release since 8.1.0 in August 2023, and the newest commit on the default branch is a documentation edit from October 2025. Dependencies are frozen at svgo 3 and prettier 2 while both are a major behind, and 122 open issues sit unaddressed
Ecosystem5/517.7M weekly downloads because it sits under @svgr/webpack, @svgr/cli, vite-plugin-svgr, Create React App, and the Next.js and Vite recipes everyone copies. The output format is effectively the convention for SVG-as-component in React

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
Skip it if

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

PackageRegistryPick it when
vite-plugin-svgrnpmYou are on Vite and just want to import an SVG as a component, with SVGR wired up and cached for you
@svgr/webpacknpmYou are on webpack, Next.js, or Rspack and want the loader form instead of calling the transform yourself
unplugin-iconsnpmYour 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
svgonpmYou only need the SVG cleaned up and want to inline the markup yourself rather than generate a component