How to svg as jsx vs. raw svg import in react: a practical comparison
- Step 1Decide if you need runtime styling or animation — If the icon must change colour with theme/state, animate a stroke, or morph a path, you need the SVG inline in the DOM — that means JSX (paste-converted) or @svgr (build-time). If it's a static decorative mark, an external
<img src>or CSS background is lighter. - Step 2Choose paste-convert vs build-time @svgr — For one icon, a prototype, or a design handoff, paste it into svg-to-jsx and copy the component — zero config. For a folder of icons regenerated on every design change, wire @svgr into webpack/Vite so the import is automatic. See the design-system guide.
- Step 3Pick the caching strategy for large art — A full-bleed hero illustration is better as an external
.svgwith a long cache header than inlined into JS — it caches once and doesn't bloat the bundle parsed on every page. Reserve inline JSX for small, frequently-reused, themeable icons. - Step 4Wire accessibility per approach — Inline SVG: add
role="img"+aria-label, oraria-hiddenfor decorative.<img>: use a meaningfulalt. The converter wires{...props}so you can passaria-*straight to the JSX component:<Logo aria-label="Acme" role="img" />. - Step 5Handle multi-render id collisions — Inline SVG with internal
<clipPath id=…>or<linearGradient id=…>collides when rendered twice. Namespace ids per instance, or prefer a single sprite (svg-sprite-builder) for icons used dozens of times. - Step 6Convert and drop in — When inline JSX is the answer, paste the SVG into the converter, set a component name, choose TSX, and copy. The output wires
{...props}andclassName, so you immediately get the theming and prop-forwarding that motivated the inline choice.
Four ways to ship an SVG in React, compared
The decisive column is 'CSS can reach inside'. Inline (JSX/@svgr) is the only approach where the page's CSS and currentColor reach the SVG's internals.
| Approach | CSS reaches inside | Animate internals | Caching | Best for |
|---|---|---|---|---|
| Inline JSX (svg-to-jsx) | Yes | Yes (CSS / WAAPI / SMIL) | In JS bundle — parsed each load | Small reused themeable icons, prototypes, handoffs |
| @svgr import (build-time) | Yes | Yes | In JS bundle, tree-shakeable | Icon libraries regenerated from a folder |
| <img src="icon.svg"> | No | No (only the whole element) | Independent HTTP cache | Static decorative marks, large one-off art |
| CSS background / data URI | No | No | Inlined in CSS or cached file | Decorative backgrounds, list bullets |
When paste-convert beats build-time @svgr (and vice versa)
Both produce inline React SVG. The difference is workflow, not output. The JAD converter is a paste tool; @svgr is a build dependency.
| Situation | Use svg-to-jsx (paste) | Use @svgr (build) |
|---|---|---|
| One icon, right now | Yes — copy in 5 seconds | Overkill |
| Prototype / spike | Yes | Not yet |
| Designer hands you 3 SVGs | Yes — paste each | Optional |
| 200-icon library, weekly redesigns | No — tedious | Yes — automate |
| You want SVGO + forwardRef baked in | No — converter does neither | Yes — configure it |
| No build access (CMS / sandbox) | Yes — paste, no loader | Can't — needs bundler config |
Accessibility pattern by approach
The a11y contract differs because the SVG is either in the DOM (inline) or an opaque image (img/background).
| Approach | Decorative | Meaningful |
|---|---|---|
| Inline JSX | aria-hidden="true" (pass as prop) | role="img" + aria-label="…" (pass as props) |
| <img> | alt="" | alt="meaningful text" |
| CSS background | Inherently decorative — fine | Don't — backgrounds aren't announced; use inline or img instead |
Cookbook
Concrete code for each approach so the trade-offs are tangible, not abstract.
Themeable inline icon (the JSX win)
Only inline SVG lets the page's color flow into the icon via currentColor. The converter output already wires {...props} and className, so theming is one class on the parent.
// Generated by svg-to-jsx (TSX):
export const BellIcon = ({ className, ...props }: React.SVGProps<SVGSVGElement>): React.ReactElement => (
<svg {...props} className={className} viewBox="0 0 24 24" fill="currentColor">…</svg>
);
// Theme by class on the parent — impossible with <img>:
<button className="text-slate-400 hover:text-blue-600">
<BellIcon className="h-5 w-5" />
</button>Static decorative mark as <img> (the cache win)
A large, never-themed illustration is better as an external file: it caches independently and never enters the JS bundle. You lose CSS reach inside, which you don't need here.
// public/hero-illustration.svg cached with a long max-age
<img
src="/hero-illustration.svg"
alt="Team collaborating around a whiteboard"
width={640}
height={420}
loading="lazy"
/>
// No JS cost, no theming — correct for static art.Same icon, two approaches, measured
Inlining a 1.2 KB icon 30 times across a page bundle vs one external file. The trade is parse cost (inline) against an extra request that caches (external). For tiny reused icons, inline usually wins on round-trips.
Inline JSX icon (1.2 KB), used on 30 routes: + ~1.2 KB in the shared chunk (parsed once, cached in JS) + 0 extra HTTP requests + full theming/animation External 1.2 KB .svg via <img>, 30 routes: + 1 HTTP request, then cached + 0 JS parse cost - no theming, no internal animation
Duplicate id collision when inlining the same icon twice
Inline SVG with internal ids breaks when rendered more than once — both instances reference the first. This is the classic inline footgun; external <img> and sprites avoid it.
// LogoIcon defines <clipPath id="clip0"> internally. <LogoIcon /> // clip0 → instance A's clip (correct) <LogoIcon /> // clip0 → STILL instance A's clip (wrong) // Fixes: // - namespace ids per instance before converting, or // - use a single <symbol> sprite via svg-sprite-builder.
Next.js: keep the icon a Server Component
The converter's output has no hooks or handlers, so it's a valid React Server Component — no 'use client', no client JS shipped for the icon. Only the interactive wrapper needs the client boundary.
// icon.tsx — Server Component (no 'use client')
export const MenuIcon = ({ className, ...props }: React.SVGProps<SVGSVGElement>) => (
<svg {...props} className={className} viewBox="0 0 24 24">…</svg>
);
// Only the button that uses onClick is a client component:
'use client';
<button onClick={toggle}><MenuIcon className="h-6 w-6" /></button>Edge cases and what actually happens
Trying to theme an <img src> SVG with CSS
Not possibleSVGs referenced via <img> are isolated documents — the page's stylesheet and currentColor do not cross the boundary. filter hacks can recolour them crudely but you cannot set per-path fills. If you need theming, you must go inline (JSX/@svgr); this is the single most common reason to reach for the converter.
Inline SVG inflating the JS bundle
By designEvery inline JSX icon is JavaScript that ships and parses with your bundle. A handful of small icons is negligible; inlining a 60 KB illustration on a route is not. Keep large, static, un-themed art as external files and reserve inline for small reused themeable icons.
Repeated inline icon with internal ids
CollisionInline SVGs that define <clipPath>, <mask>, <filter> or gradient ids collide when the same component renders multiple times — later instances reference the first element's defs. Namespace ids per instance, or use a <symbol> sprite (svg-sprite-builder) for icons used many times on one page.
Inline style strings in converted JSX
Needs manual fixIf you choose inline JSX via the converter and the source has style="…" strings, React throws because the converter does not turn them into style objects. Either move presentation into attributes before converting or refactor the style to an object after. <img> and CSS-background approaches don't hit this because the SVG is never parsed as JSX.
@svgr default export vs ReactComponent named import
Config-dependentWith @svgr, import Icon from './icon.svg' (default) vs import { ReactComponent as Icon } depends on your loader config (notably CRA vs Next/Vite differences). The JAD converter sidesteps this entirely — it emits both a named and a default export, so either import style works.
CSS data URI used for a themeable icon
Wrong toolA data URI baked into CSS cannot inherit the page's color, so you'd need a separate encoded variant per theme. For theming, use inline JSX. If you only need a static decorative background, svg-css-data-uri is the right generator — but don't expect runtime theming from it.
Hard-coded hex colours blocking currentColor theming
Pre-processInlining a JSX icon only enables currentColor theming if the SVG actually uses currentColor. An icon full of fill="#1f2937" won't follow the parent's text colour. Run it through svg-to-tailwind (current mode) or svg-hex-swapper first to swap fixed colours to currentColor, then convert to JSX.
Lighthouse flags large inline SVG blocking render
PerformanceA massive inline SVG in the initial HTML/JS can delay LCP. For above-the-fold large art, prefer an external .svg (cacheable, non-blocking) or a properly sized <img> with width/height to reserve layout. Inline is for icons, not hero illustrations.
Server Component vs Client Component confusion
SupportedConverted icons are pure renderers and work as Server Components by default — you don't need 'use client' on the icon. Only add the client directive to a wrapper that attaches interactivity. Putting 'use client' on every icon needlessly ships client JS.
Animating a path with React state
Inline onlyTo drive an SVG path/fill from React state (strokeDashoffset on scroll, animated morphs), the SVG must be inline so React owns those nodes. <img> and CSS backgrounds can only animate the element as a whole. This is a clear case for the JSX converter, then bind the animated attribute to state.
Frequently asked questions
Is inline JSX always better than importing an SVG file?
No. Inline JSX (whether pasted via svg-to-jsx or built by @svgr) is better when you need to style or animate the SVG's internals, because only inline SVG lets the page's CSS and currentColor reach inside. But inline SVG ships as JavaScript and parses with your bundle, so a large static illustration is better as an external <img src> that caches independently. Match the approach to the requirement: themeable/animated → inline; static/large → external.
What's the difference between svg-to-jsx and @svgr?
They produce the same kind of output — an inline React SVG component — but differ in workflow. svg-to-jsx is a paste tool: you give it one SVG and copy a component, no build config. @svgr is a build-time loader you wire into webpack/Vite so import Icon from './icon.svg' returns a component automatically. Use the converter for one-offs, prototypes, and handoffs; use @svgr for a folder of icons regenerated on every design change.
Can I theme an SVG used as <img src>?
Not properly. An SVG in <img> is an isolated document — your stylesheet and currentColor don't cross into it, so you can't set per-path fills from the page. CSS filter can crudely recolour the whole image but not individual elements. For real theming, convert the SVG to inline JSX so React renders the paths directly in your DOM.
Which approach is best for Core Web Vitals?
For large above-the-fold art, an external .svg (or sized <img>) with a long cache header is best — it caches independently and doesn't bloat the JS parsed on every load. For small icons reused across pages, inline JSX is fine and saves HTTP round-trips. The anti-pattern is inlining a big illustration into JS, which adds parse cost and can delay LCP.
Does using @svgr require a bundler?
Yes — @svgr is a webpack/Vite/Rollup loader (or a CLI), so it needs build tooling and config. If you're in an environment without bundler access (a CMS snippet, a sandbox, a no-build page), the paste-based svg-to-jsx converter is the practical choice: it outputs plain React you drop in with a single import React.
How do I make a converted icon accessible?
Because the converter wires {...props} onto the root <svg>, you pass a11y attributes straight through: <Logo role="img" aria-label="Acme" /> for meaningful icons, or <Icon aria-hidden="true" /> for decorative ones. With <img> you'd use alt instead. Don't rely on CSS-background SVGs for meaningful imagery — backgrounds aren't announced to screen readers.
Why does my repeated inline icon render wrong?
Almost always an id collision. Inline SVGs that define internal ids (clipPath, mask, gradients) reuse the same id across every instance, so the second render references the first's defs. Namespace the ids per instance before converting, or for icons used many times on one page use a single <symbol> sprite via svg-sprite-builder, which references by <use href="#id"> once.
Can I use CSS data URIs for theme-aware icons?
No — a data URI baked into CSS can't inherit the parent's color, so each theme would need its own encoded copy. Use inline JSX for theming. svg-css-data-uri is the right tool for static decorative backgrounds where you don't need runtime theming; it's a different job from the JSX converter.
Does inline SVG tree-shake?
With @svgr generating one module per icon and named imports, unused icons tree-shake out. Paste-converted components tree-shake the same way as any module you export and import — if you put each in its own file and only import what you use, the bundler drops the rest. A giant barrel file that re-exports everything can defeat tree-shaking in some setups, so import from the specific module when bundle size matters.
Is the converter output a Server or Client Component?
It's framework-agnostic React with no hooks or handlers, so in Next.js App Router it works as a Server Component by default — no 'use client' needed and no client JS shipped for the icon. Add the client directive only to an interactive wrapper (e.g. a button with onClick that contains the icon).
What about React Native — does the same component work?
No, not directly. React Native renders SVG via react-native-svg, whose components (<Svg>, <Path>) and some prop names differ from DOM SVG. The web JSX from this converter targets the DOM. For cross-platform, generate a separate RN variant or use a shared abstraction; svg-to-vue-svelte and this tool both target web frameworks, not native.
If I pick inline, should I still minify the SVG first?
Yes — since inline SVG becomes JavaScript in your bundle, smaller is better. Run the source through svg-pro-minifier (whitespace/comment/metadata removal) and svg-precision-tuner (round coordinates) before converting to JSX. The converter itself only strips comments and xmlns:xlink; it does not minify paths or geometry.
Privacy first
Every JAD SVG tool runs entirely in your browser using the DOM API and Canvas. Your SVG files never leave your device — verified by zero outbound network requests during processing.