Skip to main content

SVG to JSX: Turn Any SVG Into a Reusable React Icon Component

Paste an SVG, get a React component. Why JSX needs camelCased attributes, className, and self-closing tags — with a worked example and an icon pattern.

Published By Li Lei
#react #svg #jsx #frontend #icons

SVG to JSX: Turn Any SVG Into a Reusable React Icon Component

You copy an SVG off a Figma artboard, paste it into a .tsx file, and the build immediately turns red. The first stroke-width= throws a syntax error. The inline style="..." is a string where React wants an object. And even after you patch those by hand, the icon often renders as a blank square because one capital letter got lost somewhere. Raw SVG and JSX look almost identical, which is exactly why pasting one into the other so reliably fails.

The fix is not magic — it's a handful of mechanical transformations that have to happen before the markup compiles. This guide walks through every one of them, shows a small SVG becoming a real component, and ends with the icon pattern that makes the whole exercise worth it.

Why a Raw SVG Won't Compile in React

JSX is not HTML. It compiles down to React.createElement calls, and React applies SVG attributes through the DOM's SVG-element properties, which are keyed by camelCased names. So element.strokeWidth exists; element["stroke-width"] does not get wired up the way you expect. Concretely, JSX needs camelCased SVG attributes (stroke-width to strokeWidth, fill-rule to fillRule), class rewritten to className, and every empty element written as a self-closing tag — so a raw SVG must be transformed before it compiles in React.

Here are the four classes of change, all of which have to land at once:

  • Hyphenated attributes become camelCase. stroke-width to strokeWidth, fill-rule to fillRule, stroke-linecap to strokeLinecap, clip-path to clipPath, stop-color to stopColor. The hyphen in JSX reads as a minus sign, so the literal attribute is a syntax error, not just a typo.
  • class becomes className. class is a reserved word in JavaScript, so React renamed the prop. Same idea behind for becoming htmlFor on labels.
  • Inline styles become objects. style="fill:red;stroke-width:2" has to become style={{ fill: 'red', strokeWidth: '2' }} — a real JavaScript object with camelCased keys, not a CSS string.
  • Empty elements self-close. SVG bodies are full of childless elements like <path d="..."></path> or <circle .../>. JSX requires the trailing slash on any element with no children: <path d="..." />.

And here's the one that bites quietly: case-sensitive names. viewBox, preserveAspectRatio, and gradientUnits are already mixed-case and must stay exactly that way. Element names like linearGradient, clipPath, and feGaussianBlur are case-sensitive too. Lowercase any of them and the SVG renders blank with no error in the console. A naive find-and-replace across the file is the most common way to mangle this.

A Worked Example: From SVG to Component

Take a tiny checkmark icon, the kind you'd export from an icon set:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
     fill="none" stroke="currentColor" stroke-width="2"
     stroke-linecap="round" class="icon">
  <path d="M20 6L9 17l-5-5"></path>
</svg>

Three things in there will break a React build: stroke-width, stroke-linecap, and class. The viewBox is fine as long as nobody lowercases the capital B. Converted to JSX and wrapped as a component, it becomes:

export default function CheckIcon(props) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      className="icon"
      {...props}
    >
      <path d="M20 6L9 17l-5-5" />
    </svg>
  );
}

Trace the diff: stroke-width to strokeWidth, stroke-linecap to strokeLinecap, class to className, the <path> collapsed to a self-closing tag, and viewBox left untouched. The {...props} spread is the part that turns a one-off conversion into something reusable, which is the next section. Run this through the SVG to JSX converter and you get the camelCasing and self-closing automatically, including the awkward namespaced cases like xlink:href becoming xlinkHref.

Making It a Reusable Icon Component

A converted SVG that hardcodes its own color and size is barely better than an <img>. The point of having it as JSX is that props can drive everything. Two small moves get you there.

First, the {...props} spread sits on the root <svg>, after the built-in attributes. Because it comes last, anything the caller passes overrides the defaults. That single line lets every consumer set className, width, onClick, aria-label, or anything else without you anticipating it:

<CheckIcon className="size-5 text-green-600" onClick={handleClick} />

Second, lean on stroke="currentColor". Most modern icon sets ship that already, and it means the icon inherits the surrounding text color through normal CSS cascade. One component handles every color you'll ever need — no CheckIconRed, no color prop, just put it next to text styled however you like. If you also strip the hardcoded width/height, CSS or props own the size completely, which is usually what you want for an icon that appears at three different scales across an app.

That's the whole reusable-icon recipe: camelCase the attributes so it compiles, spread props so callers control it, use currentColor so it recolors itself.

A Note From the Trenches

I keep this converter pinned in a browser tab during any migration off an old codebase. The last project I touched had about forty inline SVGs written as plain HTML-ish markup — class, stroke-width, xlink:href, the works — scattered across server-rendered templates that we were porting to React. My first instinct was a regex sweep, and it cost me an afternoon: the regex happily lowercased viewBox to viewbox in a couple of files, and those icons rendered as empty boxes with zero errors to point at. I only caught it because one of them was a logo nobody could miss. Pasting each SVG through a real DOMParser-based pass instead, and copying the output, was both faster and boringly correct — every capital letter survived. The lesson stuck: for SVG-to-JSX, parse the tree, don't pattern-match the text.

Where SVG to JSX Fits in the Workflow

Converting to JSX is usually the last step, not the first. If the SVG came straight out of a design tool, it's probably carrying editor cruft — redundant groups, bloated path precision, metadata you'll never use. Clean it up first: run it through the SVG optimizer to shrink paths and drop the junk, then convert the lean result to JSX. You end up with a smaller component and a smaller bundle, instead of pasting a 4 KB Figma export verbatim into your source tree.

A few practical reminders before you ship converted output:

  • Conversion is not sanitization. A camelCasing pass rebuilds the markup; it does not scrub it. If your source SVG carries a <script> or an onload, it survives into the JSX (with the handler camelCased). Review the output before trusting an SVG from an untrusted source.
  • xlinkHref compiles but is dated. Newer React prefers a plain href on <use>. The converter emits xlinkHref because it matches existing codebases and always compiles; if your React version warns, rename it to href after pasting.
  • Name the component meaningfully. MenuIcon, LogoMark, SpinnerIcon — a good default export name saves a rename the moment you import it.

Once that's done, your SVG is no longer a fragile blob of markup. It's a typed, recolorable, prop-driven React component you can drop in anywhere and style with one class.


Made by Toolora · Updated 2026-06-13