Migrating HTML to JSX without the whack-a-mole
Every attribute rename, style-object conversion, void elements, comments, and the two errors you will hit, with a converter that does the mechanical part.
Moving a page of HTML into a React component looks like a paste. Then the compiler complains about class, then about for, then about a <br>, then about the style attribute, then about two root elements. Each fix reveals the next. The whole set of differences is small and finite, though, and once you know it the migration is mechanical — which is also why it should be done by a tool.
Why JSX is not HTML
JSX compiles to function calls, and attributes become properties on DOM objects. Two consequences drive almost every difference. First, attribute names that are JavaScript reserved words cannot be used as-is. Second, attribute names have to match the DOM property they set, and DOM properties are camelCase.
Everything below follows from those two facts.
The attribute renames
class→className,for→htmlFor. The reserved words.- Compound lowercase attributes → camelCase:
tabindex→tabIndex,readonly→readOnly,maxlength→maxLength,colspan→colSpan,srcset→srcSet,autocomplete→autoComplete,contenteditable→contentEditable. - Hyphenated SVG attributes → camelCase:
stroke-width→strokeWidth,fill-opacity→fillOpacity,xlink:href→xlinkHref. - Event handlers → camelCase and a function:
onclick="save()"→onClick={save}. The tool renames the attribute; you supply the function reference.
Two families are left alone: data-* and aria-* keep their hyphenated names. Leaving them is as important as renaming the rest.
Miss one and React tells you at runtime with Invalid DOM property, which is a warning rather than a crash — and in older versions meant the attribute was silently dropped.
The style attribute becomes an object
HTML's style="font-size: 12px; margin-top: 4px" is a string. JSX's style is an object with camelCase keys:
style={{ fontSize: 12, marginTop: 4 }}
Numbers without a unit are pixels. Vendor prefixes capitalise: -webkit-transition → WebkitTransition. Custom properties keep their dashes and need quoting: "--gap": "8px". A url(...) value containing a semicolon must not be split on it — a naïve string split gets this wrong, and it is one of the reasons to use a converter rather than a regex.
Void elements must self-close
<br>, <img src="...">, <input>, <hr>, <meta> are complete in HTML without a closing tag. JSX requires <br />, <img src="..." />. Forgetting it produces Expected corresponding JSX closing tag, which points at some later element and sends you looking in the wrong place.
Comments and braces
<!-- note --> becomes {/* note */}. And any literal { or } in text content has to be escaped, because braces open an expression: a code sample that says { "a": 1 } inside a <p> needs {"{"}, or the whole thing wrapped in a template string.
One root element
A component returns one element. A pasted fragment with an <h1> followed by a <p> produces Adjacent JSX elements must be wrapped in an enclosing tag. The fix is a fragment, <>...</>, which groups children without adding a node — not a <div>, which changes the DOM and breaks flex, grid, tables and lists.
Doing it in one pass
HTML to JSX applies the entire list: every rename in the attribute map including the SVG set, the style-string-to-object conversion with custom properties and url() handled, self-closed void elements, converted comments, escaped braces, and a fragment wrapper when the markup has more than one root. Paste the HTML, take the JSX.
What it deliberately does not do is invent components. The output is one block of JSX; splitting it into <Header />, <Card /> and so on is a design decision, and it is easier to make with working JSX in front of you than with HTML.
Before and after the conversion
Run the HTML through HTML Formatter first if it came from a page source or a CMS export: consistent indentation makes the structure — and therefore the component boundaries — visible. If the markup carries a large class vocabulary from an old stylesheet, CSS to Tailwind can map the declarations onto utility classes as part of the same migration, though that is a separate decision from the JSX conversion and worth making separately.