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 URLImport as a component
You getA string: the file’s addressA function component
Used as<img src={logoUrl} /><Logo />
The SVG ends upIn its own file, cached by the browserInline in the page, inside the JavaScript bundle
CSS and props reach the shapesNoYes: className, fill, width, event handlers
currentColor follows the text colourNoYes
NeedsNothing extra in most toolsSVGR

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:

ImportResult
'./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',
      },
    ],
  },
};
typeAn import gives
asset/resourceThe URL of a separate file
asset/inlineA data: URL
asset/sourceThe file’s text
assetA 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: true replaces the file’s width and height with 1em, so the icon scales with the font size.
  • dimensions: false removes the file’s width and height and leaves the size to props and CSS.
  • replaceAttrValues swaps 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 replaceAttrValues runs. Black is the default fill, so SVGO drops fill="#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. Write fill="currentColor" in the file instead. Strokes and other colours are swapped as expected.
  • Ids are prefixed with the file name, so id="a" in logo.svg becomes logo_svg__a. Gradients in two different files cannot collide.
  • A viewBox that only repeats the file’s width and height is removed. A file with width="100" height="100" viewBox="0 0 100 100" comes out with no viewBox, and the component then crops when a different size is passed. Setting icon: true or dimensions: false keeps 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 currentColor in 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/webpack prefixes ids for you. With vite-plugin-svgr, give the files distinct ids.

Common problems

What you seeCauseFix
“The tag name provided (‘/assets/logo.svg’) is not a valid name”A URL import used as a componentIn 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 componentSet up SVGR, or use <Image src={logo} />
“Element type is invalid … but got: undefined”import { ReactComponent } in a tool that has no such exportUse that tool’s form, such as ?react in Vite
A broken image, with src holding JavaScript codeA component import used as an srcRender it as <Logo />, or import the URL
Cannot find module './logo.svg' from TypeScriptNo type declaration for .svgAdd 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 stringsrc={logo.src}, or use <Image src={logo} />
The component is cropped when resizedThe file has no viewBox, or @svgr/webpack removed itAdd one to the file; set dimensions: false or icon: true
The colour will not changeFixed fill values in the fileReplace them with currentColor
The SVG works in development and is missing in productionThe path was written as a string, not importedImport 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.