You copy an SVG directly from Figma or an icon library, paste it straight into your React or Next.js component, and everything looks fine during local dev. Then you push to staging, and suddenly your console is flooded with hydration warnings, gradients turn black across the entire dashboard, or your production build fails completely.
Converting raw SVG markup into JSX or TSX seems simple on the surface, but XML-based SVG syntax and React JSX have fundamental structural differences. Here are 5 common SVG-to-JSX edge cases that cause production bugs and how to handle them cleanly.
1. Kebab-Case Attributes vs JSX CamelCase & Reserved Words
Standard SVG files exported from design software use standard XML kebab-case attributes. In JSX, all SVG attributes (with a few exceptions like data-* and aria-*) must be converted to camelCase.
Common attributes that cause warnings or broken rendering:
-
stroke-width->strokeWidth -
stroke-linejoin->strokeLinejoin -
stroke-linecap->strokeLinecap -
clip-path->clipPath -
fill-rule->fillRule -
stop-color->stopColor -
stroke-dasharray->strokeDasharray -
xmlns:xlink->xmlnsXlink -
xlink:href->xlinkHref
Furthermore, raw SVGs often contain HTML reserved keywords like class="icon" and for="id". In JSX, these must be className and htmlFor.
// ❌ Breaks in JSX or throws React hydration warnings
<svg stroke-width="2" stroke-linecap="round" class="w-6 h-6">
<path d="..." fill-rule="evenodd" />
</svg>
// ✅ Correct JSX mapping
<svg strokeWidth={2} strokeLinecap="round" className="w-6 h-6">
<path d="..." fillRule="evenodd" />
</svg>
2. Inline style="..." Attribute Strings
Design tools like Adobe Illustrator and Inkscape frequently export SVGs with inline CSS string declarations inside style attributes.
In JSX, passing a string to style throws an explicit runtime error:
Uncaught Error: The style prop expects a mapping from style properties to values, not a string.
// ❌ Throws runtime error in React
<circle cx="50" cy="50" r="40" style="fill: #3b82f6; stroke: #1d4ed8; stroke-width: 3px;" />
// ✅ Must be parsed into a JavaScript style object with camelCased keys
<circle
cx={50}
cy={50}
r={40}
style={{
fill: "#3b82f6",
stroke: "#1d4ed8",
strokeWidth: "3px"
}}
/>
When automating your workflow with bundlers or browser utilities like Nutilz SVG to JSX, inline CSS strings are parsed via AST into standard JSX style dictionaries with numeric dimensions and camelCased CSS properties.
3. Global ID Collisions in <defs>, <linearGradient>, and <clipPath>
This is one of the nastiest visual bugs in React applications. SVGs with gradients or masks use <defs> with unique ID references:
<defs>
<linearGradient id="gradient-a" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6" />
<stop offset="100%" stop-color="#9333ea" />
</linearGradient>
</defs>
<rect fill="url(#gradient-a)" width="100" height="100" />
When you render this SVG component multiple times on the same page, or render two different icons that happen to both have id="gradient-a", the browser DOM resolves url(#gradient-a) to whichever element appears first in the DOM tree.
As a result, all icons on your page inherit the colors or clip paths of the very first icon rendered.
The Fix: Use React 18s useId() hook to dynamically generate unique element IDs:
import React, { useId } from "react";
export function GradientIcon(props: React.SVGProps<SVGSVGElement>) {
const baseId = useId();
const gradId = `${baseId}-grad`;
return (
<svg viewBox="0 0 100 100" {...props}>
<defs>
<linearGradient id={gradId} x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stopColor="#3b82f6" />
<stop offset="100%" stopColor="#9333ea" />
</linearGradient>
</defs>
<rect fill={`url(#${gradId})`} width="100" height="100" />
</svg>
);
}
4. Unstripped XML Comments, CDATA, and DOCTYPE Declarations
Raw SVG exports often begin with XML headers and metadata:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
Placing raw <!-- comments --> inside JSX triggers syntax errors because JSX treats standard HTML comments as literal text or invalid tokens unless wrapped in {/* ... */}. Always strip XML declarations, DOCTYPE headers, and metadata tags like <metadata> before embedding inside JSX trees.
5. Color and Sizing Flexibility: currentColor vs Hardcoded Hexes
Raw SVGs usually have hardcoded width="24" height="24" and fill="#000000". In modern component libraries (using Tailwind CSS, CSS modules, or styled-components), you want icons to automatically inherit font color and size from parent buttons and badges.
Transform the root SVG tag to inherit dimensions and colors:
- Replace static
fill="#000000"withfill="currentColor" - Remove fixed
widthandheightattributes if using responsiveviewBoxsizing - Spread
...propsto allow consumer components to passclassName,onClick, andaria-label
interface IconProps extends React.SVGProps<SVGSVGElement> {
size?: number | string;
}
export function BellIcon({ size = 24, className, ...props }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
);
}
Summary
Cleaning up SVGs manually for React takes only a few minutes once you know what to look for: camelCase attributes, parsed style objects, unique useId() gradient IDs, and currentColor fills.
For rapid prototyping when pasting icons directly from Figma or vector packs, you can use the free Nutilz SVG to JSX converter to sanitize attributes, remove XML bloat, and generate clean TypeScript component wrappers directly in your browser.
Top comments (0)