Skip to content

SVG to React

SVG to React component: from path data to something you import.

The editor copies any drawing out as JSX, with the common attribute renames already applied. Everything after that is spelled out below — binding fill and stroke to props, the TypeScript typing, the Vue and React Native equivalents, the bundler setup, and the parts a copy-paste gets wrong.

The same icon, three ways

Raw markup, React, Vue.

icon.svgraw markup
<svg
  viewBox="0 0 24 24"
  fill="none">
  <path
    id="stem"
    d="M12 3v18"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"/>
</svg>
Icon.jsxreact
export function Icon(props) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      aria-hidden="true"
      {...props}>
      <path
        id="stem"
        d="M12 3v18"
        stroke="currentColor"
        strokeWidth={2}
        strokeLinecap="round"/>
    </svg>
  )
}
Icon.vuevue
<template>
  <svg
    viewBox="0 0 24 24"
    fill="none"
    aria-hidden="true">
    <path
      id="stem"
      d="M12 3v18"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"/>
  </svg>
</template>

Three details do most of the work: viewBox with no width or height so CSS controls the size, currentColor so the icon inherits text colour, and a props spread so the caller can pass a class, a label, or a click handler.

Attribute conversion

The renames JSX insists on.

Pasting raw SVG into a React file is the single most common way to get a wall of console warnings. Every hyphenated presentation attribute becomes camelCase, and inline styles become objects.

Vue does not need any of this — it keeps the attribute names from the markup as they are.

SVG

JSX

  • stroke-widthstrokeWidth
  • stroke-linecapstrokeLinecap
  • stroke-linejoinstrokeLinejoin
  • stroke-dasharraystrokeDasharray
  • fill-rulefillRule
  • clip-pathclipPath
  • clip-ruleclipRule
  • stop-colorstopColor
  • stop-opacitystopOpacity
  • xlink:hrefhref
  • classclassName
  • style="fill:red"style={{ fill: 'red' }}

Two variants worth having

TypeScript, and React Native.

One types the props so you stop writing an interface per icon. The other is not really the same conversion at all — React Native has no DOM, so the element names have to change.

Icon.tsxtypescript
import type { SVGProps } from "react";

export function Icon(props: SVGProps<SVGSVGElement>) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      aria-hidden="true"
      {...props}>
      <path
        d="M12 3v18"
        stroke="currentColor"
        strokeWidth={2}
        strokeLinecap="round"/>
    </svg>
  );
}
Icon.native.jsxreact native
import Svg, { Path } from "react-native-svg";

export function Icon({ color = "#111", size = 24 }) {
  return (
    <Svg
      viewBox="0 0 24 24"
      width={size}
      height={size}
      fill="none">
      <Path
        d="M12 3v18"
        stroke={color}
        strokeWidth={2}
        strokeLinecap="round"/>
    </Svg>
  );
}

SVGProps<SVGSVGElement> is the whole TypeScript story: spread it and every valid SVG attribute, plus className, onClick and the aria props, is typed without you maintaining anything per icon.

Build tools

Importing an .svg file as a component.

Pasting markup is right for one icon. For a set of two hundred, you want the import to do the conversion at build time — which is a different setup in every bundler.

SVG import setup by build tool
ToolPackageSetup
Vitevite-plugin-svgrAdd the plugin to vite.config, then import with the ?react suffix: import Icon from "./icon.svg?react". The suffix is what tells the plugin to hand back a component rather than a URL.
Next.js@svgr/webpackAdd a webpack rule in next.config for files matching /\.svg$/ using @svgr/webpack. Turbopack users configure the same loader under the turbopack rules key instead.
Webpack@svgr/webpackOne module rule: test /\.svg$/, use @svgr/webpack. Add asset/resource as a second use entry if you also want the URL available.
Create React Appbuilt inNothing to install. import { ReactComponent as Icon } from "./icon.svg" has worked since CRA 2, and the named ReactComponent export is the part people forget.

Most of those plugins wrap SVGR, which is the reference implementation for this transform. It converts whatever markup you give it faithfully — so the quality of the component you end up with is decided before the conversion, by how clean the SVG was.

Delivery

A component is not always the right answer.

Ways to deliver an SVG to the browser, with trade-offs
MethodSuitsGainsCosts
Inline componentBest under ~4 KBNo network request, styleable with CSS, animatable, and currentColor just works.Every instance adds its nodes to the DOM. Twenty copies of a 200-path illustration is 4,000 elements.
<img src="icon.svg">Any sizeCached by the browser, one DOM node, cannot be broken by page CSS.No currentColor, no CSS control, no interaction with anything inside the file.
Sprite + <use>Many icons at onceOne request for a whole set, one definition per symbol however many times you draw it.A build step, and cross-document references have their own caveats.
CSS backgroundDecorative onlyKeeps purely ornamental art out of the document entirely.Invisible to assistive technology, which is correct for decoration and wrong for anything else.

Accessibility

Two patterns, and knowing which you need.

An icon beside a text label is decoration and should be hidden from assistive technology. An icon that is the only thing in a button is the label, and needs a name. Getting these the wrong way round is the most common SVG accessibility bug there is.

Full accessibility guide
Decorative — beside a text label
<button>
  <Icon aria-hidden="true" />
  Save file
</button>
Meaningful — the icon is the label
<button aria-label="Save file">
  <Icon role="img" />
</button>

Questions

Shipping vectors in a codebase.

How do I make an icon inherit my text colour?
Set fill="currentColor" (or stroke="currentColor") in the markup. The icon then takes the CSS color of whatever it sits inside, so text-brand on the parent recolours it with no prop at all.
What is the difference between SVG to React and SVG to JSX?
Nothing, in practice. JSX is the syntax; a React component is JSX wrapped in a function you can import. Converting to JSX is the whole job — the wrapper is four lines around it.
Should I put width and height on the SVG?
Keep viewBox, drop width and height, and size the component with CSS classes. Hardcoded dimensions fight every layout you later drop the icon into.
Does this work with TypeScript?
Yes — type the props as React.SVGProps<SVGSVGElement> and spread them onto the root element. You then get every valid SVG attribute typed for free, including className, onClick and aria-label, with no per-icon interface to maintain. There is a full .tsx example below.
Can I import an .svg file directly instead of pasting the markup?
Yes, and for a large icon set that is the better habit. Vite, Next.js, Webpack and Create React App each have a way to turn an .svg import into a component at build time — the setup for each one is in the table below.
How does this relate to SVGR?
SVGR is the library most of those build-tool plugins wrap, and it is the reference implementation for this conversion — worth knowing about whichever route you take. The difference in approach is upstream of it: SVGR transforms whatever markup you hand it, so a 600-path traced file becomes a 600-path component. What comes out of the editor here is already grouped and reduced before it ever becomes JSX.
What about accessibility?
Decorative icons take aria-hidden="true" and no title. Meaningful ones take role="img" and an aria-label, or a <title> element referenced by aria-labelledby. An icon that is the only content of a button always needs a name.
Is inlining SVG bad for performance?
It can be, and it is worth being honest about. Every inline instance puts its nodes in the DOM, so twenty copies of a 200-path illustration is 4,000 elements the browser has to lay out. For icons of a few paths it is a non-issue; for anything heavy, use a sprite or an img tag. The delivery table above is the decision.
What about Vue?
Vue keeps SVG attribute names exactly as they are in the markup, so there is far less to rename than JSX needs — the work is mostly the props binding. Our editor's code panel outputs JSX only, so a Vue component is a short hand edit from the raw markup rather than something you can copy straight out.
React Native?
React Native has no DOM, so it cannot render svg or path elements at all — you need react-native-svg, which supplies Svg, Path, G and the rest as real components. The path data transfers unchanged and the camelCase attributes are the same; what changes is that every element name becomes a capitalised import from that library.

Generate it, then copy it as a component.

Open the editor