How to automate batch svg to react component conversion
- Step 1Get the JAD tool schema — Call
GET /api/v1/tools/svg-to-jsxwith your API key. It returns the option schema —componentName(string) andtypescript(boolean, default true) — plusacceptsMultiple: false,minTier: free, andexecution.runnerMode: engine. Build your payload from this, don't assume extra options exist. - Step 2Pair a local runner (the only upload path) — The hosted
/runendpoint is upload-free — it returns 400 pointing you to/docs/runner. Install and pair@jadapps/runner; it listens onhttp://127.0.0.1:9789. POST{ slug, text, options }per file tohttp://127.0.0.1:9789/v1/tools/svg-to-jsx/runand the engine runs on your machine. - Step 3Loop over your SVG folder — Read each
.svg, derive a PascalCasecomponentNamefrom the filename, POST it to the runner (or call the engine directly in a Node script), and write the returned component tosrc/components/icons/<Name>.tsx. Keeptypescript: truefor a typed library. - Step 4Or run @svgr/cli for SVGO + forwardRef — If you need SVGO optimisation,
forwardRef, Prettier, or title injection — none of which the JAD engine does —npm i -D @svgr/cli, add a.svgrrc(typescript: true,svgo: true,ref: true,prettier: true), and runnpx svgr --out-dir src/components/icons src/assets/icons. - Step 5Post-process the two known gaps — The JAD engine does NOT convert inline
style="…"strings to objects and does NOT namespace ids. Add a script step that rewrites style strings to objects and prefixes internal ids with the component name, so multi-render pages don't collide. @svgr handles styles via SVGO config; ids still need attention. - Step 6Generate a barrel and validate the build — Emit an
index.tsre-exporting each component, then runtsc --noEmit(and ideally a Storybook/visual snapshot) so a malformed SVG that produced invalid JSX fails CI rather than shipping.
The real JAD svg-to-jsx contract (from the API schema)
What GET /api/v1/tools/svg-to-jsx actually returns. Build your batch payloads against these fields — there are no others.
| Field | Value | Meaning for batch |
|---|---|---|
| options.componentName | string (optional) | PascalCase name; per-file, derive from filename stem |
| options.typescript | boolean, default true | true → .tsx typed React.SVGProps<SVGSVGElement>; false → .jsx |
| acceptsMultiple | false | One SVG per call — your script loops; there is no native multi-file batch |
| minTier | free | No paid gate on the conversion itself |
| execution.runnerMode | engine | Runs as pure text in the runner — no headless browser spun up |
| hosted /run | 400 'Runner required' | API never accepts uploads; POST to the local runner instead |
JAD engine vs @svgr — pick by feature need
Both turn SVGs into React components in bulk. The JAD engine is a clean attribute-rename with typed props; @svgr is the heavier, configurable toolchain.
| Capability | JAD engine (runner) | @svgr/cli |
|---|---|---|
| Attribute rename to JSX | Yes (28 attrs + drop xmlns:xlink) | Yes |
| Typed props (SVGProps) | Yes (typescript: true) | Yes (typescript: true) |
| SVGO optimisation | No | Yes (svgo: true) |
| forwardRef wrapper | No | Yes (ref: true) |
| Inline style → object | No (manual fix) | Yes (via SVGO config) |
| title/desc injection | No | Yes (titleProp) |
| Files leave your machine? | No (engine-mode runner, local) | No (local CLI) |
| Setup | Pair runner once | Install dep + .svgrrc |
Batch endpoints and where they run
The hosted API is deliberately upload-free; the local runner is the execution surface for engine-mode SVG tools.
| Endpoint | Accepts SVG content? | Runs where |
|---|---|---|
| GET /api/v1/tools/svg-to-jsx | No — returns schema | JAD (metadata only) |
| POST /api/v1/tools/svg-to-jsx/run | No — 400 + pairing info | Nowhere (gateway) |
| POST 127.0.0.1:9789/v1/tools/svg-to-jsx/run | Yes | Your machine (engine) |
| @svgr/cli | Yes (reads local files) | Your machine |
Cookbook
Runnable batch recipes. Replace paths and API keys; the JAD engine path keeps every SVG on your machine.
Discover the contract before you script
Always pull the schema first so you build payloads against the two real options, not assumed ones.
curl -s https://jadapps.com/api/v1/tools/svg-to-jsx \
-H "Authorization: Bearer $JAD_API_KEY"
# → { slug, options: [
# { name: 'componentName', type: 'string', ... },
# { name: 'typescript', type: 'boolean', default: true, ... }
# ],
# acceptsMultiple: false,
# execution: { runnerMode: 'engine' } }Batch a folder through the local runner (Node)
Loop the folder, PascalCase each filename, POST the SVG text to the paired runner, and write each returned component. Engine-mode means the conversion runs locally — nothing uploads.
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
const SRC = 'src/assets/icons', OUT = 'src/components/icons';
const pascal = s => s.replace(/\.svg$/, '').replace(/[-_.\s]+(.)/g, (_, c) => c.toUpperCase()).replace(/^./, c => c.toUpperCase());
await mkdir(OUT, { recursive: true });
for (const f of (await readdir(SRC)).filter(f => f.endsWith('.svg'))) {
const name = pascal(f);
const text = await readFile(`${SRC}/${f}`, 'utf8');
const res = await fetch('http://127.0.0.1:9789/v1/tools/svg-to-jsx/run', {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ slug: 'svg-to-jsx', text, options: { componentName: name, typescript: true } }),
}).then(r => r.json());
await writeFile(`${OUT}/${name}.tsx`, res.text ?? res.content);
}@svgr/cli when you need SVGO + forwardRef
The JAD engine doesn't optimise geometry or wrap in forwardRef. If your library standard requires those, @svgr is the right choice — one config, one command.
npm i -D @svgr/cli
# .svgrrc.json
{
"typescript": true,
"svgo": true,
"ref": true,
"prettier": true,
"jsxRuntime": "automatic"
}
npx svgr --config-file .svgrrc.json \
--out-dir src/components/icons src/assets/icons
# → one typed, optimised, forwardRef'd .tsx per .svgBake in the two manual fixes the engine skips
Add a post-pass that converts inline style strings to objects and namespaces internal ids per component, so the batch output compiles and multi-render pages don't collide.
function patch(code, name) {
// 1) style="a:b;c:d" → style={{ a: 'b', c: 'd' }} (engine leaves strings)
code = code.replace(/style="([^"]*)"/g, (_, s) => {
const obj = s.split(';').filter(Boolean).map(d => {
const [k, v] = d.split(':');
const key = k.trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
return `${key}: '${v.trim()}'`;
}).join(', ');
return `style={{ ${obj} }}`;
});
// 2) namespace internal ids so two instances don't collide
return code.replace(/id="([^"]+)"/g, `id="${name}-$1"`)
.replace(/url\(#([^)]+)\)/g, `url(#${name}-$1)`)
.replace(/href="#([^"]+)"/g, `href="#${name}-$1"`);
}Barrel export + type-check gate in CI
Re-export every component and fail the build if any generated component is invalid TSX — catches a malformed SVG before it ships.
// scripts/gen-barrel.mjs
import { readdir, writeFile } from 'node:fs/promises';
const files = (await readdir('src/components/icons')).filter(f => f.endsWith('.tsx') && f !== 'index.tsx');
const lines = files.map(f => {
const n = f.replace('.tsx', '');
return `export { ${n}, default as ${n}Default } from './${n}';`;
});
await writeFile('src/components/icons/index.tsx', lines.join('\n') + '\n');
# CI:
node scripts/gen-barrel.mjs && npx tsc --noEmitEdge cases and what actually happens
POSTing SVGs to the hosted /run endpoint
400 Runner requiredThe public POST /api/v1/tools/svg-to-jsx/run deliberately never accepts uploads — it returns 400 with error: 'Runner required', the schema endpoint, and the local runner URL. This is the privacy model: content only ever executes on your paired machine. Point your script at http://127.0.0.1:9789/v1/tools/svg-to-jsx/run instead.
Expecting a native multi-file / ZIP batch from JAD
Not supportedacceptsMultiple is false for svg-to-jsx — there is no built-in folder/ZIP conversion in the tool or engine. Batch is your loop over files calling the single-file engine per SVG. (svg-sprite-builder is the only SVG tool that takes multiple files, and it builds a <symbol> sprite, not React components.)
Inline style strings across the whole batch
Needs post-passThe engine leaves style="…" as a string, which React rejects. At batch scale a single icon family with inline styles can break dozens of components. Add a style-string-to-object post-pass (see the cookbook) or switch that subset to @svgr, whose SVGO config can normalise styles.
Duplicate ids when many icons share clip/gradient names
CollisionIcon exporters love id="clip0", id="paint0_linear". Convert 200 of them and you have 200 clip0s; render two on a page and the refs collide. Namespace ids per component in your post-pass (id="<Name>-clip0" plus the matching url(#…)/href="#…" rewrites).
Filenames that PascalCase to invalid identifiers
Build error2-factor.svg → 2Factor, which can't be a JS identifier. Your naming function must guard against leading digits (prefix a word) and sanitise non-alphanumerics. Otherwise tsc fails on export const 2Factor. Validate names in the loop before writing.
Loaded source whose root isn't <svg>
Invalid inputIf a file in the folder is a partial fragment (a <symbol> or bare <path>), the converter expects an <svg> root. Skip or wrap non-<svg> files in your loop; the engine produces a component but it may be malformed if the root assumptions don't hold.
Filter-primitive attributes left kebab-case
Dev warningAcross a batch, any icons with <filter> primitives (flood-color, lighting-color, color-interpolation-filters) keep kebab-case and warn in React. Most icon sets don't use these, but a 'duotone with shadow' set might. Add those renames to your post-pass if your library includes filters.
Free-tier byte limit in an automated loop
413-style rejectEach call is still bounded by the tier file limit (5 MB free / 50 MB Pro). Icons are tiny, but a folder mixing in full illustrations can trip it. Pre-filter by size, and minify large files with svg-pro-minifier before conversion if needed.
@svgr default vs named export differs from JAD output
Config-dependent@svgr's export style depends on its config and your framework; the JAD engine always emits both a named and a default export. If you switch generators mid-project, audit imports — import Icon from vs import { Icon } from may need updating across consumers.
Runner not running when the script fires
Connection errorIf the @jadapps/runner isn't paired/started, the POST to 127.0.0.1:9789 fails with a connection error. Add a health check (GET 127.0.0.1:9789/v1/health) at the top of the script and fail fast with a clear message pointing to /docs/runner.
Frequently asked questions
How do I batch-convert a folder of SVGs with JAD?
Loop the folder in a script and call the conversion once per file. The hosted API won't accept uploads, so pair a local @jadapps/runner and POST each SVG's text to http://127.0.0.1:9789/v1/tools/svg-to-jsx/run with options: { componentName, typescript: true }. svg-to-jsx is engine-mode, so the runner runs the conversion locally — nothing uploads. There is no native multi-file batch (acceptsMultiple is false); the loop is the batch.
Why does the public /run endpoint reject my SVG?
By design. POST /api/v1/tools/svg-to-jsx/run returns 400 Runner required and never accepts file content — that's the privacy guarantee. It hands back the schema URL and the local runner endpoint. Send your payloads to the paired runner on 127.0.0.1:9789 instead, where the engine executes on your own machine.
What options can I pass per file?
Exactly two, per the schema: componentName (a string — supply the PascalCased filename) and typescript (boolean, defaults to true → .tsx). There is no forwardRef, svgo, prettier, titleProp, or preset option in the JAD contract. If you need those, that's the signal to use @svgr/cli instead.
When should I use @svgr instead of the JAD engine?
When your component standard requires SVGO optimisation, a forwardRef wrapper, Prettier formatting, or <title> injection — none of which the JAD engine does. @svgr is a build dependency you configure once with .svgrrc. Use the JAD engine/runner when you want a clean attribute rename with typed props and you'd rather not add a build dependency, or when you want guaranteed local-only execution via the runner.
Does the JAD engine optimise the SVG like SVGO?
No. It strips XML comments and removes xmlns:xlink, but it does not run SVGO — no path merging, coordinate rounding, or defs cleanup. For smaller batch output, pre-process each file with svg-pro-minifier and svg-precision-tuner, then convert. Or use @svgr with svgo: true if you want optimisation inline.
How do I keep filenames mapping to valid component names?
PascalCase the stem (search-icon.svg → SearchIcon), but guard against invalid identifiers: prefix names that start with a digit (2fa → TwoFa) and strip stray symbols. Validate each name in the loop before writing the file, or tsc will fail on export const 2Fa. The engine PascalCases automatically when you don't pass a name, but you should pass an explicit, validated name in a batch.
Will the batch output compile without edits?
Usually, with two caveats baked into your script. First, the engine leaves inline style="…" strings unconverted — add a style-string-to-object post-pass or React will throw. Second, exporter ids like clip0 repeat across files and collide when rendered together — namespace them per component. With those two post-passes, a clean icon set converts cleanly; run tsc --noEmit in CI to be sure.
Can I run this in a GitHub Action?
Yes for the @svgr path (it's a local CLI — install dep, run npx svgr, commit). For the JAD runner path you'd need the runner available on the CI machine, which is less common in hosted CI; the runner model is built for local/dev execution where content must stay on your network. A typical setup: regenerate locally via the runner, commit, and have CI only validate with tsc --noEmit and visual snapshots.
How do I generate a barrel/index file for the icons?
After writing each <Name>.tsx, read the output directory and emit an index.ts that re-exports each component (named and/or default). This enables import { SearchIcon } from '@/components/icons'. Be aware a giant barrel can hurt tree-shaking in some bundlers — import from the specific module when bundle size is critical.
Is there a rate limit on the schema/API calls?
The hosted API enforces a per-key rate limit (returned in X-RateLimit-* headers; 429 when exceeded). But the heavy lifting — the actual conversions — happens on the local runner, not the hosted API, so a folder loop hitting 127.0.0.1:9789 isn't bounded by the hosted rate limit. You typically call the hosted API only once, to fetch the schema.
How do I verify the generated components render correctly?
Add a Storybook story or a test that mounts each component at a couple of sizes and run a visual-regression pass (Chromatic/Percy) after each batch. Pair that with tsc --noEmit so invalid JSX from a malformed source SVG fails CI. The converter can't catch a semantically-broken SVG — only a rendered snapshot will.
Can the runner handle other SVG steps in the same pipeline?
Yes — the runner exposes every engine-mode SVG tool on the same localhost surface. A common pipeline is minify (svg-pro-minifier) → scrub metadata → convert to JSX, all POSTed to the local runner in sequence, with nothing leaving your machine. Note svg-metadata-scrubber is a headless-browser tool, so it spins up a browser session in the runner rather than running as pure text.
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.