Every React developer pastes HTML into a component on day two, and every one of them hits the same wall: JSX looks like HTML but isn't. The class attribute warns, the style string throws, the <img> tag that never closes becomes a syntax error. The good news is that the distance between HTML and JSX is almost entirely mechanical — a fixed list of renames and rewrites — which is exactly the kind of work a converter does perfectly. Here's the full list, why React made these choices, and the boundary where automation stops.
Because JSX compiles to function calls, not to markup. When you write <div className="hero">, Babel turns it into React.createElement('div', {className: 'hero'}) — a JavaScript object describing the element. Attribute names in JSX become keys of a props object, and since they eventually set DOM properties (not attributes), React chose the DOM property names: element.className, element.tabIndex, element.htmlFor. HTML named them differently twenty years earlier, and the mismatch is permanent now.
| HTML | JSX | Reason |
|---|---|---|
class | className | DOM property name |
for | htmlFor | for is reserved in JS |
tabindex, readonly, maxlength, autocomplete | tabIndex, readOnly, maxLength, autoComplete | camelCase properties |
colspan, rowspan, srcset | colSpan, rowSpan, srcSet | camelCase properties |
style="color:red" | style={{color: 'red'}} | style is an object in React |
<br>, <img>, <input> unclosed | self-closed <br /> | JSX demands closed tags |
onclick="fn()" | onClick={fn} | events are props, functions |
value on an input | defaultValue (usually) | uncontrolled default |
Two of those deserve a beat. Style strings: the object's keys camel-case too, so font-size becomes fontSize, and values stay strings ('red'). value on inputs: a plain value makes the input controlled — React now expects you to hold that state; if you just pasted markup, defaultValue is the behavior the HTML had.
SVG adds a second rename list on top. React's SVG attributes are camelCase like everything else, so stroke-width, stroke-linecap, fill-rule, clip-path, and xlink:href must become strokeWidth, strokeLinecap, fillRule, clipPath, and xlinkHref. Paste an icon unchanged and nothing crashes — you just get a console full of "Invalid DOM property" warnings and an icon with no stroke, which is somehow more annoying than a crash. The exceptions prove the rule: aria-* and data-* keep their hyphens because React passes them through verbatim, and viewBox already is camelCase.
Converting markup is half the job; the other half is wrapping it as a component the way icon libraries expect. The svgr toolchain (what powers most design-system pipelines) turns an SVG file into:
export default function ActivityIcon(props)<svg {...props}> — so callers set width, className, or aria-label themselvesprops: React.SVGProps<SVGSVGElement>You can reproduce that output per-icon in a browser converter (the one below has an SVG tab that does exactly this), or at build time with svgr if every icon in a folder needs it. The pattern to internalize: the component owns the geometry, the caller owns the presentation.
Three things stay manual, always:
onclick="doThing()" is a string evaluated at click time; React wants onClick={doThing} — a function reference from your scope. A converter can rename the attribute; only you can supply the function.<script> and <link> don't map to React at all — move them into the app's build or effect logic.Everything else — the renames, the style objects, the tag closing, the SVG camelCase, the wrapper — is mechanical, and mechanical work belongs to machines.
Runs the open-source react-magic converter in your browser: attribute fixes, SVG renames, component wrappers, TypeScript. No upload.
Open the HTML to JSX ConverterThe one with provenance. React's own HTML→JSX playground ran on an open-source converter (htmltojsx, BSD-licensed, from the react-magic project) for years, and that engine is still the most battle-tested implementation of the rename list. The ToolAspect converter runs it locally in your tab — no upload, which matters if the markup is proprietary. For bulk pipelines, wire svgr into your build; for one-off pastes while coding, a browser tab beats a terminal round-trip.
Adjacent conversions, while you're here: turning a web page into Markdown for docs, cleaning up icons in an SVG editor, or rasterizing them for non-React contexts with SVG to PNG.