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.
<svg
viewBox="0 0 24 24"
fill="none">
<path
id="stem"
d="M12 3v18"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"/>
</svg>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>
)
}<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-widthstrokeWidthstroke-linecapstrokeLinecapstroke-linejoinstrokeLinejoinstroke-dasharraystrokeDasharrayfill-rulefillRuleclip-pathclipPathclip-ruleclipRulestop-colorstopColorstop-opacitystopOpacityxlink:hrefhrefclassclassNamestyle="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.
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>
);
}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.
| Tool | Package | Setup |
|---|---|---|
| Vite | vite-plugin-svgr | Add 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/webpack | Add 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/webpack | One module rule: test /\.svg$/, use @svgr/webpack. Add asset/resource as a second use entry if you also want the URL available. |
| Create React App | built in | Nothing 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.
| Method | Suits | Gains | Costs |
|---|---|---|---|
| Inline component | Best under ~4 KB | No 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 size | Cached 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 once | One 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 background | Decorative only | Keeps 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<button> <Icon aria-hidden="true" /> Save file </button>
<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.
