How to transform svg files into clean react jsx components
- Step 1Paste SVG source or drop a .svg file — Paste raw
<svg>…</svg>markup into the textarea, or drop a single.svgfile onto the upload box (one file — this tool is not multi-file; the dashed area accepts.svg/image/svg+xml). Pasted content is validated: the root element must be<svg>or you getInvalid SVG — could not parse the pasted content as valid SVG XML. - Step 2Set the PascalCase component name — Type a name in the Component name field, e.g.
SearchIcon. Leave it blank and a file upload defaults to the PascalCased file stem (search-icon.svg→SearchIcon); a paste with no name defaults toInput(from the syntheticinput.svg). The name drives both theexport constidentifier and the download filename. - Step 3Choose JSX or TSX with the TypeScript checkbox — Leave TypeScript (TSX) ticked (the default) to emit a
.tsxfile with({ className, ...props }: React.SVGProps<SVGSVGElement>). Untick it for a plain.jsxcomponent with the same destructuring but no type annotation. Both produce identical runtime markup. - Step 4Process and read the output panel — Click Process SVG. The right panel shows the generated component (truncated to 2,000 characters in the preview) plus Original and Output byte sizes. The output always begins with
import React from 'react';and ends with a named export andexport default. - Step 5Copy to clipboard or download the file — Click Copy to clipboard to grab the full component, or Download TSX/JSX to save
<ComponentName>.tsx. The 'Copy' button copies the entire component even though the on-screen preview is truncated. - Step 6Patch the two things the converter leaves alone — Inline
style="…"strings are NOT converted to objects — React will throw on them, so refactor any element with an inline style (see the cookbook). And duplicateidvalues across two icons on the same page collide forclipPath/linearGradientrefs — give each generated component unique ids if you render several at once.
The two options this converter actually exposes
The web UI for svg-to-jsx has exactly two controls. There is no preset picker, no forwardRef toggle, no SVGO step, and no batch ZIP in the in-browser tool — those belong to other tools or the API/runner path.
| Control | Type | Default | Effect |
|---|---|---|---|
| Component name | Text (PascalCased) | File stem on upload; Input on paste | Sets the export const <Name> identifier and the download filename <Name>.tsx/.jsx |
| TypeScript (TSX) | Checkbox | On | On → .tsx typed React.SVGProps<SVGSVGElement>. Off → .jsx, same destructuring, no type annotation |
Attributes the converter rewrites (and the ones it does not)
A fixed list of 28 SVG attributes is renamed to JSX camelCase, plus xmlns:xlink is deleted. Anything not on this list passes through verbatim — fine for most, but a few will warn or break in React.
| SVG attribute | Becomes | Handled? |
|---|---|---|
| class | className | Yes |
| for | htmlFor | Yes |
| tabindex | tabIndex | Yes |
| xlink:href | href | Yes |
| clip-path, clip-rule | clipPath, clipRule | Yes |
| stroke-width / -linecap / -linejoin / -dasharray / -dashoffset / -miterlimit / -opacity | strokeWidth, strokeLinecap, strokeLinejoin, strokeDasharray, strokeDashoffset, strokeMiterlimit, strokeOpacity | Yes (all 7) |
| fill-rule, fill-opacity | fillRule, fillOpacity | Yes |
| stop-color, stop-opacity | stopColor, stopOpacity | Yes |
| font-size / -family / -weight, text-anchor, dominant-baseline | fontSize, fontFamily, fontWeight, textAnchor, dominantBaseline | Yes |
| vector-effect, shape-rendering, text-rendering, marker-start/-mid/-end | vectorEffect, shapeRendering, textRendering, markerStart/Mid/End | Yes |
| xmlns:xlink | (removed) | Yes — deleted |
| style="fill:#f00" (string) | left as a string → React throws | No — refactor to a style object by hand |
| color-interpolation-filters, flood-color, flood-opacity, lighting-color | left as kebab-case → React dev warning | No — rename by hand if you keep the filter |
| xmlns (root) | kept as-is | Left in — React warns but renders |
What goes in vs. what comes out
The component wrapper is fixed: a React import, a destructured-props arrow function, the rewritten SVG with spread props, and dual exports.
| Stage | Output |
|---|---|
| Import line | import React from 'react'; |
| Signature (TSX) | export const Name = ({ className, ...props }: React.SVGProps<SVGSVGElement>): React.ReactElement => ( |
| Signature (JSX) | export const Name = ({ className, ...props }) => ( |
| Root element | <svg {...props} className={className} …rewritten attrs…> |
| Exports | Named export const Name plus export default Name; |
Cookbook
Real before/after for a typical icon export. The converter handles the attribute renames; you handle inline styles and id collisions.
A Lucide-style icon: class + stroke-* renamed, props wired
The classic case — a stroked, currentColor outline icon. class becomes className (then overwritten by the spread className={className}), and the seven stroke-* attributes camelCase. Paste this and tick TSX.
Input (pasted SVG):
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.3-4.3"/>
</svg>
Output (componentName = SearchIcon, TypeScript on):
import React from 'react';
export const SearchIcon = ({ className, ...props }: React.SVGProps<SVGSVGElement>): React.ReactElement => (
<svg {...props} className={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.3-4.3"/>
</svg>
);
export default SearchIcon;Gradient icon: stop-color / stop-opacity / xlink:href all fixed
Defs-heavy logos use stop-color, stop-opacity and xlink:href (or the modern href). All three are on the rename list, and xmlns:xlink is deleted, so the gradient survives the conversion.
Input fragment:
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#6366f1" stop-opacity="1"/>
<stop offset="1" stop-color="#a5b4fc" stop-opacity=".6"/>
</linearGradient>
</defs>
<rect width="24" height="24" fill="url(#g)"/>
Output fragment:
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stopColor="#6366f1" stopOpacity="1"/>
<stop offset="1" stopColor="#a5b4fc" stopOpacity=".6"/>
</linearGradient>
</defs>
<rect width="24" height="24" fill="url(#g)"/>Inline style string — the one you must patch by hand
The converter does NOT parse style="…" into a React style object. The string passes straight through and React throws The 'style' prop expects a mapping from style properties to values, not a string. Refactor to an object after copying.
Input:
<svg ... style="display:block;fill:#1f2937">…</svg>
Converter output (UNCHANGED — still a string):
<svg {...props} className={className} ... style="display:block;fill:#1f2937">…</svg>
Runtime: React throws on the string style.
Hand fix after copy:
<svg {...props} className={className} ... style={{ display: 'block', fill: '#1f2937' }}>…</svg>Default component name from the file stem
Drop arrow-right.svg with the name field blank and you get ArrowRight and ArrowRight.tsx. The PascalCase step splits on -, _, spaces and .. Pasting with no name yields Input (the synthetic input.svg stem), so always set a name when pasting.
Upload file: arrow-right.svg (name field left blank) → export const ArrowRight = ... / file: ArrowRight.tsx Upload file: nav_menu icon.svg (name field left blank) → export const NavMenuIcon = ... / file: NavMenuIcon.tsx Paste source, name blank: → export const Input = ... / file: Input.tsx (rename it!)
Sizing and theming the generated component
Because {...props} and className are wired on the root, you control size and colour from the call site. A currentColor icon inherits the text colour, and Tailwind size classes flow through. No size prop is generated by this tool — you pass width/height or classes directly.
import { SearchIcon } from '@/components/icons/SearchIcon';
<SearchIcon className="h-5 w-5 text-slate-500" aria-hidden />
<SearchIcon width={32} height={32} stroke="#6366f1" />
// currentColor icons inherit color from the parent:
<button className="text-blue-600 hover:text-blue-800">
<SearchIcon className="h-4 w-4" /> Search
</button>Edge cases and what actually happens
Pasted markup whose root is not <svg>
InvalidThe paste path validates with isValidSvg: the parsed root element must be <svg> and there must be no <parsererror>. A snippet that starts with <g> or <path>, or HTML wrapping the SVG, is rejected with Invalid SVG — could not parse the pasted content as valid SVG XML. Wrap the fragment in a real <svg viewBox=…> first.
Inline style="..." strings survive verbatim
Needs manual fixUnlike @svgr, this converter has no style-string-to-object pass for svg-to-jsx output. Any element carrying style="fill:#f00;…" keeps the string, and React throws at render because the style prop must be an object. Convert each one to style={{ fill: '#f00' }} by hand, or strip presentation into attributes before converting.
Filter attributes outside the rename list warn in dev
Dev warningThe 28-attribute list covers icon paint, stroke, stop and marker attributes but not filter-primitive ones like flood-color, flood-opacity, lighting-color or color-interpolation-filters. Those pass through as kebab-case and React logs an 'unknown attribute' warning (the filter usually still renders). Rename them to camelCase by hand if you keep the <filter>.
Two icons on a page share an id (clipPath / gradient collision)
ExpectedThe converter preserves id values verbatim — it does not namespace them. If you render LogoA and LogoB and both define <clipPath id="clip0">, the second reference resolves to the first element's clip in the live DOM. Give each source SVG unique ids before converting, or post-process ids to include the component name.
Root xmlns is kept, not removed
PreservedOnly xmlns:xlink is deleted. The primary xmlns="http://www.w3.org/2000/svg" stays on the root <svg>. React tolerates it (it may log a minor warning in some versions) and the icon renders correctly. Delete it manually if you want the absolutely-minimal element.
Component name with leading digits or symbols
Needs manual fixPascalCasing 2fa-icon.svg produces 2faIcon, which is not a valid JS identifier (cannot start with a digit). The file downloads but export const 2faIcon won't compile. Set a valid name like TwoFactorIcon in the Component name field before processing.
SMIL animation tags (<animate>, <animateTransform>)
PreservedSMIL elements are not on the rename list and pass straight through. React does not strip them, so they keep animating in the browser. Their own attributes (attributeName, dur, repeatCount) are already camelCase-free names React accepts, so no manual fix is usually needed.
File over the tier byte limit
413-style rejectFree tier caps the input at 5 MB; Pro at 50 MB. A larger file is blocked before processing with File "…" exceeds the <tier> tier per-job limit. Icons are kilobytes, so this only bites on huge illustrative SVGs — minify with svg-pro-minifier first, or split the artwork.
data-* and aria-* attributes
Supporteddata-* and aria-* attributes are valid JSX as-is and are not on the rename list, so they pass through unchanged and work correctly. No action needed — aria-hidden, aria-label, data-testid all survive.
Comments inside the SVG
By designXML comments (<!-- … -->) are stripped during conversion. This is intentional cleanup — exporter banners and licence comments are removed from the component body. If you need to retain a licence header, add it back as a JS comment above the component after copying.
Frequently asked questions
Does it convert every SVG attribute to camelCase automatically?
No — and it's important to know the boundary. It rewrites a fixed list of 28 attributes (class, for, tabindex, xlink:href, the seven stroke-*, fill-rule, fill-opacity, stop-color, stop-opacity, clip-path, clip-rule, the font/text attributes, vector-effect, the rendering hints, and the three markers) and deletes xmlns:xlink. These are the attributes React rejects or warns on for typical icons. Attributes outside the list — filter primitives like flood-color, or any inline style string — pass through unchanged. For icons that's complete; for filter-heavy artwork, patch the leftovers by hand.
Why does my converted component throw on the style prop?
Because the converter does not turn style="fill:#f00" strings into React style objects — that string passes through verbatim, and React's style prop only accepts an object. Refactor each inline style to style={{ fill: '#f00' }} after copying, or remove the inline styles in the SVG source (move them to presentation attributes) before converting. Tools like @svgr do this string-to-object step; this lightweight converter does not.
Can I get a .tsx file with proper typing?
Yes. Leave the TypeScript (TSX) checkbox ticked and the component is typed ({ className, ...props }: React.SVGProps<SVGSVGElement>): React.ReactElement. That gives you autocomplete on every standard SVG and DOM prop — width, viewBox, onClick, aria-label, data-* — with no hand-written interface. Untick it for a plain .jsx component with the same runtime shape.
How do I size the icon — is there a size prop?
There is no generated size prop. The root <svg> receives {...props} and className={className}, so you size it from the call site: <SearchIcon width={32} height={32} /> or <SearchIcon className="h-8 w-8" />. If the source SVG had a viewBox, the icon scales correctly to whatever width/height or CSS size you pass. If you want a typed size prop helper, wrap the generated component yourself, or use svg-to-vue-svelte for frameworks that generate one.
Will currentColor icons still inherit colour after conversion?
Yes. fill="currentColor" and stroke="currentColor" are values, not attributes, so they're untouched by the rename pass. The component inherits the parent's CSS color, so <span className="text-red-500"><SearchIcon/></span> renders the icon red. If your SVG uses hard-coded hex instead and you want currentColor behaviour, run it through svg-to-tailwind (current mode) first.
Does it work in Next.js App Router server components?
Yes. The output is a pure rendering function — no hooks, no event handlers, no 'use client'. It renders fine as a React Server Component, which is the better default for icons in Next.js 13+. Only add 'use client' to a wrapper if you attach interactive handlers like onClick to the icon.
What's the default component name if I leave the field blank?
On a file upload it's the PascalCased file stem — chevron-down.svg → ChevronDown. On a paste with no name it defaults to Input, because the pasted text becomes a synthetic input.svg. Always type a name when pasting so you don't end up with export const Input. Note the name must be a valid JS identifier, so avoid leading digits (2fa → set TwoFactor).
Can it convert multiple SVGs at once into a ZIP?
Not in this in-browser tool — it processes a single file or one pasted source and gives you one component to copy or download. For automating many icons, see the batch guide at /svg-tools/guides/batch-svg-to-react-component-automation, which uses the API/runner and @svgr patterns. The svg-sprite-builder tool is the only SVG tool here that takes multiple files at once, but it builds a <symbol> sprite, not React components.
Is my SVG uploaded to a server?
No. The conversion runs in your browser via the in-page processor — the file or pasted text is read locally and the component is generated client-side. The result panel even shows a '0 bytes uploaded' badge. This matters when the icon is an unreleased brand mark or product logo you can't send to a third party.
What does the output file look like structurally?
Always: import React from 'react';, a blank line, export const <Name> = ({ className, ...props })… => ( then the rewritten <svg {...props} className={className} …> body, );, a blank line, and export default <Name>;. You get both a named and a default export, so import SearchIcon and import { SearchIcon } both work.
Does it run SVGO / minify the SVG before converting?
No. It strips XML comments and deletes xmlns:xlink, but it does not minify paths, merge groups, round coordinates, or remove unused defs. If you want a smaller component, run the SVG through svg-pro-minifier or svg-precision-tuner first, then convert the minified output to JSX.
Can I automate this in a build pipeline?
Yes. GET /api/v1/tools/svg-to-jsx returns the two-option schema (componentName, typescript). The public /run endpoint never accepts uploads — it returns 400 with pairing instructions — so you pair a local @jadapps/runner and POST the payload to http://127.0.0.1:9789/v1/tools/svg-to-jsx/run. The runner executes the same engine code on your machine, so the SVG never leaves your network. svg-to-jsx is an engine-mode tool (no headless browser needed).
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.