SVG in React: inline JSX, components and props

How to use SVG in React: write it inline as JSX, rename attributes such as stroke-width to strokeWidth, and build icon components that take size and colour.

Published

React draws SVG the same way it draws HTML: write the <svg> element and its shapes in JSX and they become real elements in the page. React supports every SVG element. The one job when pasting SVG code into a component is renaming a few attributes, because JSX uses camelCase: stroke-width becomes strokeWidth, and class becomes className.

function CheckIcon() {
  return (
    <svg viewBox="0 0 24 24" width="24" height="24" fill="none"
      stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M5 13l4 4L19 7" />
    </svg>
  );
}

<CheckIcon /> now draws a tick anywhere in the app, in the colour of the text around it.

Three ways to show an SVG

MethodCodeUse it when
Inline JSX<svg>...</svg> in a componentThe SVG must change: colour, size, hover, animation, data
An image<img src={logoUrl} alt="..." />The picture never changes and should be cached as a file
Import as a componentimport Logo from './logo.svg?react'You want inline SVG but would sooner keep the artwork in .svg files

This page covers the first. The other two depend on the build tool, and importing SVG in React has the setup for Vite, Next.js and webpack.

The attributes that change

JSX attributes become JavaScript property names, so names with a hyphen or a colon are written in camelCase.

In an SVG fileIn JSX
classclassName
stroke-widthstrokeWidth
stroke-linecap, stroke-linejoinstrokeLinecap, strokeLinejoin
stroke-dasharray, stroke-dashoffsetstrokeDasharray, strokeDashoffset
fill-opacity, stroke-opacityfillOpacity, strokeOpacity
fill-rule, clip-rulefillRule, clipRule
clip-pathclipPath
stop-color, stop-opacitystopColor, stopOpacity
font-size, font-family, text-anchorfontSize, fontFamily, textAnchor
xlink:hrefxlinkHref, or better, plain href
xml:spacexmlSpace
style="fill: red"style={{ fill: 'red' }}

Some things stay exactly as they are:

  • Attributes that are already camelCase in SVG: viewBox, preserveAspectRatio, gradientUnits, patternUnits, attributeName.
  • aria-* and data-* attributes, which keep their hyphens: aria-label, aria-hidden, data-id.
  • Element names, in their SVG spelling: <linearGradient>, <clipPath>, <foreignObject>. A capital first letter would mean a React component.

Two more rules of JSX apply. Every tag must be closed, so <path d="..." /> and not <path d="...">. And style takes an object, not a string: passing a string is an error that stops the component rendering.

If a hyphenated name slips through, React still passes it to the page, so the drawing looks right, and it logs a warning in development such as Invalid DOM property `stroke-width`. Did you mean `strokeWidth`?. Fix these: the console is where React tells you about real problems, and a wall of warnings hides them.

xmlns="http://www.w3.org/2000/svg" is not needed on an inline <svg> in JSX. Leaving it in does no harm. SVG xmlns explains where it is required.

Clean the SVG before pasting

Code exported from a drawing program carries comments, an <?xml ?> line, editor data and long numbers. Comments, the <?xml ?> line and a DOCTYPE are syntax errors in JSX, and the rest is clutter in a component. Run the file through the SVG optimizer first, then rename the attributes.

For a file with many attributes to rename, a converter does it faster than a hand can. The React documentation recommends using one, and SVGR is the usual tool: it is what build tools use to turn .svg files into components.

An icon component with props

The point of inline SVG is that props can reach it. A useful icon takes a size and passes everything else to the <svg>:

function Icon({ size = 24, children, ...props }) {
  return (
    <svg viewBox="0 0 24 24" width={size} height={size} fill="none"
      stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
      aria-hidden="true" {...props}>
      {children}
    </svg>
  );
}

function SearchIcon(props) {
  return (
    <Icon {...props}>
      <circle cx="11" cy="11" r="7" />
      <path d="M21 21l-4.3-4.3" />
    </Icon>
  );
}

<SearchIcon size={16} className="muted" /> draws the icon at 16 pixels with a class for CSS to use.

  • Colour comes from currentColor, which takes the CSS color of the surrounding text. Set color on the parent, or with a class, and the icon follows. Changing SVG colour with CSS explains it.
  • Size is set by width and height. The viewBox stays fixed at the size the icon was drawn in, which is what lets it scale: see SVG viewBox explained.
  • {...props} after the defaults lets the caller override any of them and add className, style, onClick or aria-label.

Accessible names

An icon next to text that says the same thing should be hidden from screen readers, which is why the component above sets aria-hidden="true". An icon that stands alone needs a name:

function CloseButton({ onClick }) {
  return (
    <button type="button" aria-label="Close" onClick={onClick}>
      <svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"
        stroke="currentColor" strokeWidth="2" strokeLinecap="round">
        <path d="M6 6l12 12M18 6L6 18" />
      </svg>
    </button>
  );
}

For an SVG that is a picture in its own right, give the <svg> role="img" and an aria-label, or a <title> as its first child. Since React 19, a <title> rendered in a component is normally moved to the document’s <head>. React makes an exception for a <title> inside an <svg>, which stays where it is. SVG title and alt text covers the choices.

Drawing from data

JSX can build shapes from an array, which is all a simple chart is:

function BarChart({ values }) {
  const max = Math.max(...values);
  const barWidth = 100 / values.length;
  return (
    <svg viewBox="0 0 100 50" role="img" aria-label="Bar chart">
      {values.map((value, i) => (
        <rect key={i} x={i * barWidth + 1} width={barWidth - 2}
          y={50 - (value / max) * 50} height={(value / max) * 50} fill="teal" />
      ))}
    </svg>
  );
}

<BarChart values={[3, 7, 4, 9]} /> draws four bars. Numbers can be passed as numbers: x={12} and x="12" give the same attribute.

Shapes are painted in the order they are rendered, and SVG has no z-index. To bring one item to the front, render it last: see SVG z-index.

Ids that do not collide

Gradients, clipping paths, masks and patterns are used by id, as in fill="url(#fade)". If a component with a fixed id is rendered twice, the page has two elements with the same id, and every copy uses the first one it finds. React’s useId hook gives each instance its own:

import { useId } from 'react';

function FadeSwatch({ from, to }) {
  const id = useId();
  return (
    <svg viewBox="0 0 100 20" width="200" height="40">
      <defs>
        <linearGradient id={id}>
          <stop offset="0" stopColor={from} />
          <stop offset="1" stopColor={to} />
        </linearGradient>
      </defs>
      <rect width="100" height="20" fill={`url(#${id})`} />
    </svg>
  );
}

Two swatches with different colours now each show their own gradient.

An SVG that arrives as a string

SVG markup fetched from a server or a content system is text, not JSX. React will insert it with dangerouslySetInnerHTML:

function RawSvg({ markup }) {
  return <span className="svg-holder" dangerouslySetInnerHTML={{ __html: markup }} />;
}

The name is a warning. An SVG can carry scripts and event handlers, so do this only with markup from your own files, or after sanitising it. For a file you control, importing it as a component is safer and gives you props.

Measuring and animating

A ref gives access to the real element, for the methods SVG adds to the DOM:

import { useEffect, useRef, useState } from 'react';

function PathLength({ d }) {
  const ref = useRef(null);
  const [length, setLength] = useState(0);
  useEffect(() => {
    setLength(ref.current.getTotalLength());
  }, [d]);
  return (
    <svg viewBox="0 0 100 100" width="100" height="100">
      <path ref={ref} d={d} fill="none" stroke="currentColor" />
      <text x="4" y="96" fontSize="8">{Math.round(length)} units</text>
    </svg>
  );
}

CSS animation works on inline SVG in React as it does anywhere: put the keyframes in a stylesheet and a className on the shape, as in SVG animation with CSS. SVG’s own <animate> elements work in JSX too, and their attribute names, such as attributeName and repeatCount, need no changes.

Common problems

What you seeCauseFix
Invalid DOM property `stroke-width` in the consoleA hyphenated attributeRename it: strokeWidth
Invalid DOM property `class` class in JSXclassName
The component crashes with “The style prop expects a mapping”style="..." as a stringstyle={{ fill: 'red' }}
Build error: unexpected tokenA comment, an <?xml ?> line or a DOCTYPE in the pasted codeRemove them, or optimise the file first
Build error: “Namespace tags are not supported”A tag or attribute with a colon in its name, such as <sodipodi:namedview> or xmlns:xlink, in a project compiled with BabelDelete editor elements and rename the attributes, or optimise the file first
Nothing is drawnNo viewBox and a small size, or a fill the same colour as the pageAdd the viewBox, and check fill and stroke
Every copy shows the first copy’s gradientThe same id rendered more than onceuseId
CSS colour has no effectThe paths have their own fill="#000"Replace it with currentColor
The icon will not resizewidth and height are fixed and there is no viewBoxAdd viewBox, pass the size as a prop

Questions

Is inline SVG or an img tag better in React? Inline for icons and anything that changes with state or CSS. An <img> for large illustrations that never change, since the browser caches the file and the markup stays out of the JavaScript bundle.

Does inline SVG make the bundle bigger? Yes. The markup becomes JavaScript that is downloaded, parsed and run. Hundreds of small icons are fine. A detailed illustration is better as a file.

Does this work with TypeScript? Yes. Type the props of an icon component as React.SVGProps<SVGSVGElement> and the SVG attributes passed to it are checked.

Do I need an icon library? No. A component is a few lines, as above. Libraries save drawing the icons, not writing the code.