Import SVG in React: Vite, Next.js and webpack
How to import an SVG file in React: as a URL for an img tag, or as a component with SVGR. The setup for Vite, Next.js, webpack and Create React App.
Published
React itself does not import SVG files. The build tool does, and it can hand your component one of two things: the file’s address, to put in an <img>, or a React component that renders the SVG inline. Which you get from import logo from './logo.svg' depends on the tool and how it is set up.
import logoUrl from './logo.svg';
function Header() {
return <img src={logoUrl} alt="Plotcaster" width="120" height="32" />;
}
This form, the file as a URL, works with no setup in Vite and in Create React App, and with one short rule in webpack.
URL or component
| Import as a URL | Import as a component | |
|---|---|---|
| You get | A string: the file’s address | A function component |
| Used as | <img src={logoUrl} /> | <Logo /> |
| The SVG ends up | In its own file, cached by the browser | Inline in the page, inside the JavaScript bundle |
| CSS and props reach the shapes | No | Yes: className, fill, width, event handlers |
currentColor follows the text colour | No | Yes |
| Needs | Nothing extra in most tools | SVGR |
Use a URL for logos and illustrations that never change. Use a component for icons that take a colour from CSS, or anything that responds to state. SVG in React covers writing the inline form by hand.
Vite
These results are from Vite 8.3 with vite-plugin-svgr 5.2.
As a URL. Works out of the box:
import logoUrl from './logo.svg';
For a file of 4 KiB or more, the value in development is the path of the source file, such as /src/logo.svg, and in a production build it is the address of a copy with a hash in its name, such as /assets/logo-BijImnRF.svg. A smaller SVG is not served as a file at all: Vite writes it into the JavaScript as a data: URL, which still works as an src. The limit is the build.assetsInlineLimit option.
A suffix on the import changes what comes back:
| Import | Result |
|---|---|
'./logo.svg' | A URL, or a data: URL for a small file |
'./logo.svg?no-inline' | Always a URL to a separate file |
'./logo.svg?inline' | Always a data: URL |
'./logo.svg?raw' | The file’s text, as a string |
'./logo.svg?react' | A React component. Needs the plugin below |
A file in the public folder is not imported. Refer to it by its path from the root: <img src="/logo.svg" /> for public/logo.svg.
As a component. Install the plugin:
npm install --save-dev vite-plugin-svgr
Add it to the configuration:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
export default defineConfig({
plugins: [react(), svgr()],
});
Then import with ?react on the end:
import Logo from './logo.svg?react';
function Header() {
return <Logo width={120} height={32} className="logo" aria-label="Plotcaster" />;
}
The props are passed to the <svg> element. An import without ?react still gives a URL, so both forms can be used in one project.
TypeScript. Without type declarations, TypeScript reports Cannot find module './logo.svg' or its corresponding type declarations. The declarations come from vite/client, which types a .svg import as a string, and from vite-plugin-svgr/client, which types a ?react import as a component. List both in tsconfig.json:
{
"compilerOptions": {
"types": ["vite/client", "vite-plugin-svgr/client"]
}
}
A project made from Vite’s React template already lists vite/client. The other way to load them is a declaration file, such as src/vite-env.d.ts, holding these two lines:
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
Next.js
This section follows the Next.js 16 and SVGR documentation.
As an image. Put the file in the public folder and refer to it by path, or import it and pass the import to the Image component:
import Image from 'next/image';
import logo from './logo.svg';
export default function Header() {
return <Image src={logo} alt="Plotcaster" />;
}
In Next.js a static image import is not a string. It is an object that carries the address with the width and height, made for <Image>, which is why width and height can be left out here. For a plain <img>, use logo.src.
Next.js does not run SVG files through its image optimiser by default: a vector needs no resizing, and an SVG can carry scripts. An src ending in .svg is treated as unoptimised automatically. The dangerouslyAllowSVG option switches optimisation on for SVG, and the documentation attaches security advice to it.
As a component. Install @svgr/webpack and add a rule. Since Next.js 16 the default bundler is Turbopack, which can run this loader:
// next.config.js
module.exports = {
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
};
import Logo from './logo.svg';
export default function Header() {
return <Logo width={120} height={32} />;
}
With this rule an imported .svg is a component, so the Image example above no longer applies to imported files. Keep files meant for <Image> in public.
For a project that still builds with webpack, which in Next.js 16 means passing the --webpack flag, the SVGR documentation gives the equivalent: push a rule with test: /\.svg$/ that uses the @svgr/webpack loader on to config.module.rules inside the webpack(config) function of the Next.js configuration.
TypeScript. Next.js types a .svg import as any, so that it does not clash with SVGR. To type the imports as components, the SVGR documentation says to add a declaration file and list it first in the include array of tsconfig.json:
// svgr.d.ts
declare module '*.svg' {
import { FC, SVGProps } from 'react';
const content: FC<SVGProps<SVGElement>>;
export default content;
}
webpack
webpack 5 handles files with asset modules, which replaced file-loader, url-loader and raw-loader. No loader needs installing for a URL import:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.svg$/i,
type: 'asset/resource',
},
],
},
};
type | An import gives |
|---|---|
asset/resource | The URL of a separate file |
asset/inline | A data: URL |
asset/source | The file’s text |
asset | A data: URL for files under 8 kB, a separate file otherwise |
For components, install @svgr/webpack and use it as the loader. This configuration, from the SVGR documentation, supports both forms: ./logo.svg?url gives a URL and ./logo.svg gives a component.
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.svg$/i,
type: 'asset',
resourceQuery: /url/, // *.svg?url
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
resourceQuery: { not: [/url/] }, // exclude react component if *.svg?url
use: ['@svgr/webpack'],
},
],
},
};
The issuer line applies SVGR only to imports made from JavaScript and TypeScript files, so an SVG used in a stylesheet as url(./logo.svg) is left alone.
Create React App
Create React App supports both forms with no configuration, from react-scripts 2.0:
import logoUrl from './logo.svg';
import { ReactComponent as Logo } from './logo.svg';
The first is the URL. The second, with the name ReactComponent in curly braces, is the component. The React team deprecated Create React App in February 2025 and recommends moving to a framework or to a build tool such as Vite. In Vite the second line becomes import Logo from './logo.svg?react'.
What SVGR does
Every component import above is the work of SVGR. At build time it reads the .svg file, converts the attributes to JSX names such as strokeWidth, and wraps the result in a component that passes its props to the <svg> element. The output is the component you would have written by hand.
It takes options, set in the bundler configuration or in a .svgrrc file. Three are worth knowing:
icon: truereplaces the file’swidthandheightwith1em, so the icon scales with the font size.dimensions: falseremoves the file’swidthandheightand leaves the size to props and CSS.replaceAttrValuesswaps one attribute value for another, for example{ "#333": "currentColor" }to make a dark grey icon follow the text colour.
@svgr/webpack also optimises each file with SVGO before converting it, which has three effects worth knowing. Tested with version 8.1:
- A plain black fill is removed before
replaceAttrValuesruns. Black is the default fill, so SVGO dropsfill="#000"from any shape that would be black without it, and a rule such as{ "#000": "currentColor" }then finds nothing to swap. The icon stays black. Writefill="currentColor"in the file instead. Strokes and other colours are swapped as expected. - Ids are prefixed with the file name, so
id="a"inlogo.svgbecomeslogo_svg__a. Gradients in two different files cannot collide. - A
viewBoxthat only repeats the file’swidthandheightis removed. A file withwidth="100" height="100" viewBox="0 0 100 100"comes out with no viewBox, and the component then crops when a different size is passed. Settingicon: trueordimensions: falsekeeps the viewBox:
use: [{ loader: '@svgr/webpack', options: { dimensions: false } }],
vite-plugin-svgr does not run SVGO unless you add it, so it does none of these things.
Prepare the file
What the import gives you is only as good as the file.
- Keep the
viewBox. Without it the SVG cannot be resized by props or CSS. SVG viewBox explained shows how to add one. - Use
currentColorin place of fixed colours on icons, so that the colour comes from CSS. See changing SVG colour with CSS. - Remove editor data and shorten the numbers with the SVG optimizer. A component import puts the whole file into the JavaScript bundle, so every byte is downloaded with the app.
- Watch the ids. Gradients and clipping paths are found by id. Two different files that both use
id="a", both rendered inline on one page, will pick up each other’s gradients.@svgr/webpackprefixes ids for you. With vite-plugin-svgr, give the files distinct ids.
Common problems
| What you see | Cause | Fix |
|---|---|---|
| “The tag name provided (‘/assets/logo.svg’) is not a valid name” | A URL import used as a component | In Vite add ?react; in other tools set up SVGR |
| “Element type is invalid … but got: object” | In Next.js, an image import used as a component | Set up SVGR, or use <Image src={logo} /> |
| “Element type is invalid … but got: undefined” | import { ReactComponent } in a tool that has no such export | Use that tool’s form, such as ?react in Vite |
A broken image, with src holding JavaScript code | A component import used as an src | Render it as <Logo />, or import the URL |
Cannot find module './logo.svg' from TypeScript | No type declaration for .svg | Add vite/client, or a declare module '*.svg' file |
A broken image in Next.js, with src="[object Object]" | The import is an object, not a string | src={logo.src}, or use <Image src={logo} /> |
| The component is cropped when resized | The file has no viewBox, or @svgr/webpack removed it | Add one to the file; set dimensions: false or icon: true |
| The colour will not change | Fixed fill values in the file | Replace them with currentColor |
| The SVG works in development and is missing in production | The path was written as a string, not imported | Import the file, or move it to public |
Questions
Which form is faster? For a large illustration, the URL: the file is cached separately and is not parsed as JavaScript. For small icons, the component: there is no extra request for each one.
Can I import an SVG dynamically by name? With URLs, yes, in Vite: new URL(`./icons/${name}.svg`, import.meta.url).href gives the address of any file in that folder, because Vite finds the files that could match when it builds. The Vite documentation notes that this pattern does not work with server-side rendering. Components are usually imported one file at a time and collected in one module that exports them by name. In Vite, import.meta.glob('./icons/*.svg', { query: '?react', import: 'default', eager: true }) gathers a whole folder of them, keyed by path.
Can I get the SVG as a string? Yes: ?raw in Vite, asset/source in webpack. Insert it only if you trust the file. SVG in React shows how.
Does this work in React Native? Not as written. React Native has no <svg> element. See SVG in React Native.