Forty-two minutes. That’s the context-switch tax I lost last Tuesday wrestling a single navigation icon. I dropped a raw Figma export into VS Code, immediately hit the JSX friction wall: class instead of className, hyphenated attributes triggering ESLint, and legacy xlink:href tags throwing the TSX compiler into a syntax error. I manually renamed props, wrapped it in a functional component, and wrote type definitions. By the time I finished, my flow was dead.
This isn't an edge case; it's a daily grind. I tried cloud-based converters. They're slow, stuffed with third-party telemetry, and I'm absolutely not feeding unreleased design assets to a remote subdomain. I even spun up a Node script with SVGR's CLI. It handled clean exports fine, then immediately choked on a complex Illustrator file. Mixed XML namespaces and nested layer groups broke the AST parser. I needed instant, deterministic, offline processing.
The Core Engine: DOMParser & Attribute Mapping
I architected this as a Chrome Extension running entirely under Manifest V3. The transformation pipeline relies on the native DOMParser API, executing synchronously in an isolated background context. I pipe the raw SVG string, traverse the DOM tree, and apply a strict dictionary for camelCase conversions.
Here’s the conceptual mapping logic:
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, "image/svg+xml");
const ATTR_MAP = {
class: 'className',
'stroke-width': 'strokeWidth',
'fill-rule': 'fillRule',
'stroke-linecap': 'strokeLinecap'
};
function mapAttributes(node) {
Array.from(node.attributes).forEach(attr => {
const targetName = ATTR_MAP[attr.name] || attr.name.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
if (targetName !== attr.name) {
node.removeAttribute(attr.name);
node.setAttribute(targetName, attr.value);
}
});
Array.from(node.children).forEach(mapAttributes);
}
The Namespace Crash & JSX Resolution
DOMParser handles raw XML fine, but browsers play fast and loose with XML namespaces. React’s JSX compiler does not. My initial build crashed the minute I dropped in an animated chart SVG. It packed xmlns:xlink declarations and inline <use> tags referencing xlink:href. Modern browsers parse this silently, but when serialized to JSX, the unparsed namespace prefixes and deprecated attributes throw immediate syntax errors during the tsdx/esbuild transpilation step.
I had to implement a recursive cleanup pass before serialization:
-
Namespace Stripping: Explicitly walk the tree, detect
xmlns:xlink, and drop the attribute declaration from the root. -
Legacy Migration: Query all
<use>and<a>tags. Copy thexlink:hrefvalue to a standardhrefattribute, then safely deletexlink:href. -
Security Hardening: Explicitly block and sanitize
<script>,<foreignObject>, and inline event handlers (onload,onclick) during traversal. This forces the pipeline to strictly output declarative, safe markup.
Once the attribute resolver locked in, the transformer became fully synchronous.
Security: Manifest V3 Sandbox & Zero Network Requests
Because this runs under Manifest V3, the entire pipeline executes in a strictly sandboxed background service worker. There are zero fetch calls, zero CDN dependencies, and absolutely no external server routing. Every transformation happens clientside in memory. If your design files contain proprietary dashboard UI logic, they never leave the local machine. The isolated execution model guarantees deterministic output without telemetry or external payload validation.
Production Workflow & TypeScript Integration
I wired the workflow to Alt+S. Hit the shortcut, paste the markup, or drag a .svg directly into the popup. You get clean JSX in under 200ms. Handling edge cases manually got tedious, so I baked in the production features I actually use:
-
Batch Processing: Drop dozens of icons, output a single bundled
.tsfile or a structured.zip. -
React Hydration: Auto-wraps exports in
React.memo, injectsforwardRefso tooltip/popover libraries don't panic, and swaps hardcoded hex values tocurrentColorfor seamless CSS variable overrides. -
Strict TypeScript: Bakes in
React.SVGProps<SVGSVGElement>definitions out of the box. The compiler stays quiet, and IntelliSense works immediately.
Shipping it brought the usual infra headaches. I wired up chrome.i18n from day one, shipping RU, EN, DE, ES, FR, PT, ZH, and JA. It felt like overhead until download velocity spiked from Germany and Japan. For licensing, I initially tried Lemon Squeezy but hit domain verification walls. Migrated to Gumroad, skipped the compliance queue, and shipped a lifetime access checkout. It killed 2 AM support tickets and simplified the revenue pipeline.
Conclusion
Developers don't hate configuration; we hate losing flow. Tools that demand logins, network handshakes, and confirmation modals break context. Local execution isn't a technical flex—it's the baseline for serious engineering workflows. I built this to stop bleeding time on Figma exports while deadlines loomed. Turns out, the friction was universal.
If you want to cut the manual cleanup tax and process SVGs entirely offline, you can grab the extension here:
Drop a comment if you're running into specific namespace edge cases or have ideas for the next AST traversal pass.
Top comments (3)
The namespace crash with xmlns:xlink is one of those SVG→JSX traps that doesn't show up until you hit a complex file from a real design tool. Most converters handle the happy path fine and silently break on animated charts or
One thing I've found with MV3 background service workers: if your DOMParser transformation takes longer than ~30 seconds on batch processing, the service worker can get terminated mid-job. Chrome's MV3 model doesn't guarantee long-running workers. Have you hit this with large batch drops? A possible fix is chunking the batch and checkpointing state via chrome.storage.local between chunks.
Also — shipping 8 languages from day one is bold for a utility extension. Gumroad over Lemon Squeezy is the right call for a lifetime deal model. The domain verification step on LSP is a real friction point for solo builders.
How do you handle SVG parsing errors with DOMParser in a TypeScript setup? I've had issues with it in the past and would love to swap ideas on this.
I'd appreciate any advice from experienced developers❤️