Every React developer hits this task eventually: you have a chunk of HTML, from a designer's handoff, a marketing template, an old static site, or copied straight out of DevTools, and you need it inside a React component. Paste it in unchanged and the compiler complains, or worse, it compiles and silently misbehaves. JSX looks like HTML but it is not HTML; it is syntax for JavaScript function calls, and that difference produces a specific, learnable set of transformations.
This guide covers every one of them, including the cases most cheat sheets skip.
Why JSX is not HTML
JSX compiles to React.createElement calls (or the modern jsx runtime equivalent). The attribute names you write become JavaScript object keys, which is why reserved words like class and for cannot be used, and why attributes follow DOM property naming (camelCase) instead of HTML attribute naming (lowercase). Once you internalize "I am writing JavaScript objects, not markup," every rule below makes sense.
The core transformations
1. class becomes className, for becomes htmlFor
<!-- HTML -->
<label for="email" class="form-label">Email</label>
// JSX
<label htmlFor="email" className="form-label">Email</label>
These are the famous two because class and for are JavaScript reserved words. React maps them to the DOM properties className and htmlFor.
2. Inline styles become objects
This is the transformation people get wrong most often, because it has three sub-rules: the value is an object not a string, property names are camelCased, and values are strings (or numbers, which React treats as pixels for most properties).
<!-- HTML -->
<div style="background-color: #1a1a2e; margin-top: 16px; z-index: 10">
// JSX
<div style={{ backgroundColor: '#1a1a2e', marginTop: 16, zIndex: 10 }}>
Note the double braces: the outer pair says "JavaScript expression here," the inner pair is an object literal. Also note zIndex: 10 stays unitless because z-index is a unitless property; React knows the difference and will not append px to it.
3. Self-close every void element
HTML lets you write <br>, <img src="...">, <input type="text">. JSX requires XML-style self-closing:
<img src="/logo.png" alt="Logo" />
<input type="text" name="q" />
<br />
<hr />
This applies to all void elements: area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr.
4. camelCase nearly every attribute
Multi-word HTML attributes become camelCase DOM property names:
tabindexbecomestabIndexreadonlybecomesreadOnlymaxlengthbecomesmaxLengthautocompletebecomesautoCompletespellcheckbecomesspellCheckcontenteditablebecomescontentEditablecrossoriginbecomescrossOrigin
The two exceptions that stay lowercase with hyphens: data-* and aria-* attributes pass through unchanged. aria-label stays aria-label, never ariaLabel.
5. Event handlers change shape entirely
<!-- HTML -->
<button onclick="submitForm()">Send</button>
// JSX
<button onClick={submitForm}>Send</button>
Three changes at once: camelCase name (onClick), braces instead of quotes, and you pass a function reference rather than a string of code to evaluate. If you are converting legacy markup with inline handlers, this is where the real porting work begins, because those handler strings reference global functions that need to become component logic.
6. Comments
<!-- HTML comment -->
{/* JSX comment */}
HTML comments are invalid inside JSX. Convert them to JavaScript comments wrapped in braces, or delete them.
The cases the cheat sheets skip
SVG attributes
Pasted SVG is full of hyphenated attributes, and almost all of them camelCase in JSX: stroke-width becomes strokeWidth, fill-rule becomes fillRule, clip-path becomes clipPath, stop-color becomes stopColor. Namespaced attributes convert too: xlink:href becomes xlinkHref (though plain href works in modern browsers and is preferred). A single exported icon can need a dozen of these changes, which is exactly the kind of mechanical work you should not do by hand.
Boolean attributes
HTML boolean attributes like disabled, checked, and required work bare in JSX (<input disabled />), but two have special handling: a controlled checked or value without an onChange handler triggers a React warning. For markup you are porting as static content, use defaultChecked and defaultValue instead.
Whitespace behaves differently
JSX trims leading and trailing whitespace on lines and collapses blank lines. Text that relied on a newline between inline elements rendering as a space can lose that space. If a converted nav suddenly has its links jammed together, add explicit {' '} spacers.
Unescaped entities and special characters
Literal < and { characters in text content will break the parse or be interpreted as expressions. Escape them as < and {'{'}, or wrap the whole string in an expression: {'a < b and {braces}'}. HTML entities like and © work fine in JSX text.
dangerouslySetInnerHTML for embedded HTML
Sometimes the right move is not converting at all. CMS content, rich text output, and third-party embeds belong in dangerouslySetInnerHTML:
<div dangerouslySetInnerHTML={{ __html: cmsContent }} />
The name is a deliberate warning: only do this with HTML you trust or have sanitized, because it is a direct XSS vector otherwise.
A worked example
<!-- Before: HTML -->
<div class="card" style="padding: 24px; border-radius: 8px">
<img src="avatar.jpg" class="avatar">
<!-- user info -->
<label for="bio">Bio</label>
<textarea id="bio" maxlength="200" spellcheck="false"></textarea>
</div>
// After: JSX
<div className="card" style={{ padding: 24, borderRadius: 8 }}>
<img src="avatar.jpg" className="avatar" alt="" />
{/* user info */}
<label htmlFor="bio">Bio</label>
<textarea id="bio" maxLength={200} spellCheck={false} defaultValue="" />
</div>
Count the changes in even this small block: two className, one style object, one self-closed image, one comment conversion, one htmlFor, two camelCased attributes, and a textarea converted to use defaultValue. Now imagine a 300-line template.
Converting at scale: a porting strategy
For a one-off snippet, convert and move on. For a whole template or static site, a little process pays off:
- Convert markup first, extract components second. Get a single giant component compiling and rendering correctly before you start splitting it into pieces. Mixing conversion errors with refactoring errors makes both harder to find.
- Hunt the inline handlers before you start. Search the source for
onclick=,onchange=, andonsubmit=. Each one is a piece of behavior that needs a home in component state or props, and knowing the count up front tells you whether this is an afternoon or a week. - Decide what stays as raw HTML. Footers full of legal text, CMS-driven sections, and embed codes are often better left as
dangerouslySetInnerHTMLislands than converted line by line. Convert what will become interactive; embed what will not. - Diff the rendered output. After conversion, compare the rendered DOM against the original page. Whitespace collapse and dropped attributes show up immediately in a structural diff and almost never in a casual visual check.
Stop doing this by hand
Every transformation in this guide is mechanical, which means a tool should do it. Our free HTML to JSX Converter handles all of it: class to className, style strings to objects, self-closing void elements, attribute camelCasing including SVG, and comment conversion, instantly and entirely in your browser. Paste your markup, copy valid JSX, and spend your time on the part a tool cannot do: turning static markup into a living component. If you are cleaning up messy source before converting, run it through the HTML Formatter first so you can actually read what you are porting.
