Shipping raw <img> icons in React costs you CSS theming, accessibility props, and tree-shaking, and dropping inline SVG into JSX bloats your components. The fix is SVGR: import a .svg file and get back a real React component you can style with currentColor, pass className and title to, and bundle only where it is used.
This guide wires up vite-plugin-svgr for a Vite project and @svgr/webpack for Next.js, adds the TypeScript module declarations both need, and starts from a minified IconVectors SVG that already paints with currentColor and keeps a clean viewBox.
Export a clean, themable SVG from IconVectors
- Open or create your icon:
- File → Open… (Ctrl+O) or New Icon (Ctrl+N).
- Set fills/strokes to
currentColorso the icon inherits CSS/text color in React.
Export a minified SVG via File → Export → Export Minified (Shift+Ctrl+M) to strip metadata and keep a clean viewBox.
Vite (React + TypeScript): install & configure SVGR
- Install
npm i -D vite-plugin-svgr # or: pnpm add -D vite-plugin-svgr # or: yarn add -D vite-plugin-svgr - Configure
vite.config.tsimport { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import svgr from 'vite-plugin-svgr' export default defineConfig({ plugins: [ react(), svgr({ svgrOptions: { // Keep viewBox for proper scaling; strip width/height svgo: true, svgoConfig: { plugins: [ { name: 'preset-default', params: { overrides: { removeViewBox: false } } }, 'removeDimensions' ] }, titleProp: true } }) ] }) - Add TypeScript types (project root)
// env.d.ts /// <reference types="vite/client" /> declare module '*.svg?react' { import * as React from 'react' const Component: React.FC<React.SVGProps<SVGSVGElement> & { title?: string }> export default Component } - Import & use
import CheckIcon from './icons/check.svg?react' export function Button() { return ( <button className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700"> <CheckIcon className="w-5 h-5" aria-hidden="true" /> Save </button> ) }Theming: because the SVG paints with
currentColor, the icon inherits the button's text color. Override it with a utility class such astext-rose-600or an inlinestyle={{ color: '#2563eb' }}.
Next.js: configure SVGR and import SVG components
- Install
npm i -D @svgr/webpack # (Next already includes SVGO; we'll keep viewBox and remove width/height) - Configure
next.config.js/** @type {import('next').NextConfig} */ const nextConfig = { webpack(config) { // Let SVGR handle .svg imports as React components config.module.rules.push({ test: /\.svg$/i, issuer: /\.[jt]sx?$/, use: [{ loader: '@svgr/webpack', options: { svgo: true, svgoConfig: { plugins: [ { name: 'preset-default', params: { overrides: { removeViewBox: false } } }, 'removeDimensions' ] }, titleProp: true } }] }) return config } } module.exports = nextConfig - Add TypeScript types (project root)
// svg.d.ts declare module '*.svg' { import * as React from 'react' const Component: React.FC<React.SVGProps<SVGSVGElement> & { title?: string }> export default Component } - Import & use
import CheckIcon from '@/icons/check.svg' export default function Page() { return ( <main className="p-6 text-emerald-600"> <h1 className="text-xl font-semibold flex items-center gap-2"> <CheckIcon className="w-6 h-6" aria-hidden="true" /> Profile updated </h1> </main> ) }
Default vs named export, and the ?react query
Most "Element type is invalid" errors, and icons that render as a broken image, come from mixing two SVGR conventions. Which one applies depends on the integration.
- vite-plugin-svgr: opt in per import with the
?reactquery, and the component is the default export. A plain import with no query returns the asset URL instead, which is what you want for an<img src>. - @svgr/webpack (the Next.js loader configured above): the default export is the component, so
import Icon from './x.svg'gives you the component directly. - Create React App: the component is the named export
ReactComponent, and the default export is the URL.
// vite-plugin-svgr
import Star from './star.svg?react' // React component (default export)
import starUrl from './star.svg' // URL string, for <img src={starUrl} />
// Create React App (@svgr/webpack, named export)
import { ReactComponent as Star } from './star.svg' // component
import starUrl from './star.svg' // URL
Rule of thumb: ?react plus a default import in Vite, { ReactComponent } in a Create React App project. Copying a CRA import into a Vite app, or the reverse, is the pitfall. The ?react query is the convention in current vite-plugin-svgr (v4); older majors transformed .svg imports via the named ReactComponent export, no query.
Tree-shaking & project structure
- Import icons directly from the files where they are used (for example
import Icon from './x.svg'). Each SVGR component is its own module, so any icon you never import is dropped from the bundle. - Barrel files (
icons/index.ts) can defeat that if the package is treated as side-effectful. If you use a barrel, set"sideEffects": falsein the icons package so unused re-exports are still eliminated. - Props: SVGR components forward standard
SVGProps<SVGSVGElement>, so you can passclassName,title,role,aria-label, and event handlers straight through.
When an SVG sprite beats components
SVGR is the right default when you theme icons individually and tree-shake them per route. It stops scaling in two cases. When the same icon repeats many times on a page (a status glyph on every table row), each instance inlines the full SVG into the DOM. And when you ship hundreds of distinct icons that mostly render on first paint, those inlined paths add up in the HTML or the bundle. An SVG sprite defines each symbol once and every use references it with <use href="#id">, so the geometry is stored once and each icon on the page is a single short element. If that matches your usage, build one with Create an SVG sprite from icons.
Troubleshooting
- Icon will not change color — a path still has a hard-coded
fill="#..."orstroke="#...". Switch the themable layer tofill="currentColor"orstroke="currentColor". - Icon scales or crops wrong — SVGO stripped the
viewBox. Keep it by settingremoveViewBox: falseand removing explicitwidth/height, exactly as in the configs above. - TypeScript cannot resolve the import — add the matching module declaration (
env.d.tsfor Vite's*.svg?react,svg.d.tsfor the Next.js*.svgimport).
Related guides
- React SVG Icons (use case)
- Vue 3 SVG Icons with Vite
- How to Use SVG Icons in Web Applications
- Create an SVG Sprite from Icons
Start Making SVG Icons Today with IconVectors
Download the fully-functional 30‑Day Free Trial and unlock your icon design workflow.
Version 1.70 for Windows, macOS, and Linux