You have a working website. Hundreds of pages, blog posts, product cards — all in plain HTML. Then you decide to migrate to Next.js. And suddenly every class needs to become className, every for needs to become htmlFor, every style="color: red" needs to become style={{ color: 'red' }}. Multiply that by two hundred articles.
That's exactly the situation I was in. I had an old content site built with static HTML, and I was porting it to Next.js — component by component, page by page. The HTML structure was fine. The logic was fine. But the JSX syntax differences were burning hours I didn't have. Copy, paste, get a red error, fix class, fix for, fix the inline styles, repeat.
That's why I built the HTML to JSX Converter — and the reverse JSX to HTML Converter. This guide explains what they do, when you need them, and exactly what transformations happen under the hood.
Why JSX Is Not HTML (Even Though It Looks Like It)
JSX is a JavaScript syntax extension. It looks like HTML, it renders like HTML, but it compiles to React.createElement() calls — which means it lives inside JavaScript and must follow JavaScript rules.
HTML was designed to be forgiving and markup-centric. JSX was designed to sit comfortably inside .js and .tsx files without conflicts. That's why the attribute names are different.
| The difference | HTML | JSX | Why it changed |
|---|---|---|---|
| CSS class | class="card" |
className="card" |
class is a reserved word in JavaScript |
| Label binding | for="email" |
htmlFor="email" |
for is a reserved word in JavaScript |
| Event handlers | onclick="fn()" |
onClick={fn} |
camelCase convention + function reference, not string |
| Multi-word events | onmouseenter |
onMouseEnter |
camelCase required for all on* attributes |
| Inline styles | style="color: red" |
style={{ color: 'red' }} |
Must be a JavaScript object, not a CSS string |
| CSS property names | font-size: 14px |
fontSize: '14px' |
kebab-case → camelCase to match DOM style properties |
| Void elements |
<br>, <input>
|
<br />, <input />
|
JSX requires all elements to be explicitly closed |
| HTML comments | <!-- text --> |
{/* text */} |
HTML comment syntax is not valid inside JSX |
| Boolean attributes |
readonly, disabled
|
readOnly, disabled
|
Some are renamed; standalone booleans work in both |
| Numeric attributes | tabindex="1" |
tabIndex={1} |
Numbers are passed as JS expressions, not strings |
These are systematic differences — they apply everywhere, every time. Which is exactly what a converter is for.
When Do You Actually Need an HTML to JSX Converter?
Most developers encounter this problem in one of these scenarios:
| Situation | What you have | What you need |
|---|---|---|
| Migrating a static site to React / Next.js | Hundreds of HTML templates and articles | JSX components ready to paste into your codebase |
| Using AI-generated code | HTML output from ChatGPT, Claude, Copilot | Valid JSX you can drop into a component |
| Converting design exports | Figma-to-HTML, Webflow export, email template | React-compatible JSX without manual attribute hunting |
| Copying UI library examples | HTML-first component docs (Bootstrap, Tailwind CDN examples) | JSX to use inside your React project |
| Building email template previews | React component for email rendering | Plain HTML for email client compatibility |
| Writing documentation | React component markup | HTML snippet for docs, README, Storybook notes |
| Sharing with non-React devs | JSX component structure | Standard HTML a backend dev or designer can read |
| CMS integration | Headless CMS preview HTML | JSX for your Next.js front end |
The migration case is the biggest one. When you move from a WordPress blog, a Jekyll site, a Webflow export, or any legacy HTML-based setup to a React or Next.js project, you face exactly the same problem at scale. The content is correct. The HTML structure is correct. But JSX won't compile until every attribute is renamed.
Doing this by hand on ten components is annoying. Doing it on two hundred pages is a week of pure mechanical work.
How the HTML to JSX Converter Works
The HTML to JSX converter applies a series of deterministic transformation rules to your input. It runs entirely in your browser — no file is uploaded, no server is involved, nothing leaves your tab.
Here's what it does, in order:
1. HTML comments → JSX expression comments
<!-- This is a comment -->
→
{/* This is a comment */}
2. Attribute renaming
All known HTML→JSX attribute pairs are renamed:
-
class→className -
for→htmlFor -
tabindex→tabIndex -
readonly→readOnly -
maxlength→maxLength -
minlength→minLength -
contenteditable→contentEditable -
autocomplete→autoComplete -
autofocus→autoFocus -
crossorigin→crossOrigin -
cellpadding→cellPadding -
rowspan→rowSpan -
colspan→colSpan - and more
3. Event handler camelCasing
All on* attributes are converted to camelCase, including the non-obvious multi-word ones:
| HTML | JSX |
|---|---|
onclick |
onClick |
onchange |
onChange |
onmouseenter |
onMouseEnter |
onmouseleave |
onMouseLeave |
onkeydown |
onKeyDown |
onkeyup |
onKeyUp |
ondblclick |
onDoubleClick |
ontouchstart |
onTouchStart |
ontouchend |
onTouchEnd |
onpointerdown |
onPointerDown |
ondragstart |
onDragStart |
onanimationstart |
onAnimationStart |
ontransitionend |
onTransitionEnd |
4. Inline style string → JavaScript object
style="color: red; font-size: 14px; margin-top: 8px"
→
style={{ color: 'red', fontSize: '14px', marginTop: '8px' }}
Every CSS property is camelCased (font-size → fontSize), and the whole thing becomes a JS object expression.
5. Void element self-closing
<br> → <br />
<img src="photo.jpg" alt="photo"> → <img src="photo.jpg" alt="photo" />
<input type="text"> → <input type="text" />
Full Example: HTML → JSX
<!-- Input HTML -->
<div class="card" onclick="handleClick()">
<label for="email">Email address</label>
<input type="email" id="email" readonly tabindex="1"
style="border: 1px solid #ccc; font-size: 14px; border-radius: 4px">
<br>
<img src="avatar.png" alt="User avatar">
<p style="color: red; font-weight: bold;">Required field</p>
</div>
// Output JSX
<div className="card" onClick={handleClick}>
<label htmlFor="email">Email address</label>
<input type="email" id="email" readOnly tabIndex={1}
style={{ border: '1px solid #ccc', fontSize: '14px', borderRadius: '4px' }} />
<br />
<img src="avatar.png" alt="User avatar" />
<p style={{ color: 'red', fontWeight: 'bold' }}>Required field</p>
</div>
Zero manual edits needed. Paste, convert, use.
How the JSX to HTML Converter Works
The JSX to HTML converter runs all the same rules in reverse.
What it converts:
| JSX | HTML |
|---|---|
className="card" |
class="card" |
htmlFor="email" |
for="email" |
onClick={fn} |
onclick="fn" |
tabIndex={1} |
tabindex="1" |
readOnly |
readonly |
style={{ color: 'red', fontSize: '14px' }} |
style="color: red; font-size: 14px" |
<br /> |
<br> |
<img src="…" /> |
<img src="…"> |
<div /> |
<div></div> |
{/* comment */} |
<!-- comment --> |
<>…</> |
unwrapped children |
<React.Fragment>…</React.Fragment> |
unwrapped children |
JSX expression values are also resolved:
| JSX attribute | HTML output |
|---|---|
disabled={true} |
disabled (bare boolean attribute) |
hidden={false} |
removed entirely |
data-value={null} |
removed entirely |
tabIndex={1} |
tabindex="1" (value stringified) |
aria-label={"Close"} |
aria-label="Close" |
Style object → CSS string:
style={{ backgroundColor: '#fff', fontSize: '16px', zIndex: 100, WebkitTransform: 'translateX(10px)' }}
→
style="background-color: #fff; font-size: 16px; z-index: 100; -webkit-transform: translateX(10px)"
PascalCase vendor prefixes (WebkitTransform) are correctly converted to hyphenated CSS (-webkit-transform). Unitless numbers (zIndex: 100) are preserved as-is.
What the Converter Does NOT Handle (And Why That's Fine)
JSX is a superset of HTML. Some JSX constructs have no HTML equivalent and the converter intentionally leaves them intact so you can see what needs manual work:
| JSX construct | Why it can't auto-convert | What to do |
|---|---|---|
{variable} |
JavaScript value resolved at runtime | Replace with the actual rendered value |
{condition && <Element />} |
Runtime conditional | Manually output the element or nothing |
<Button variant="primary" /> |
React component, not an HTML tag | Replace with the HTML the component renders |
key={id} |
React-internal prop, no DOM output | Drop it — HTML has no key concept |
ref={myRef} |
React-internal prop | Drop it |
dangerouslySetInnerHTML |
React-specific API | Extract the __html value manually |
This is by design. The converter handles the mechanical part — attribute naming, style syntax, self-closing tags, fragment unwrapping. The dynamic and component-level logic you keep.
The Migration Workflow: Static Site to Next.js
Here's the actual workflow if you're in the same situation I was — porting a content site to Next.js:
Start with the page layout. Convert your
<header>,<nav>,<footer>HTML to JSX. The converter handles the structural attributes instantly.Handle content blocks. For article bodies with mixed HTML (headings, paragraphs, lists, code blocks), paste each section and convert. If the content is CMS-driven, you'll likely render it with
dangerouslySetInnerHTMLanyway — in which case the original HTML is what you want.Convert form elements. Forms are where HTML→JSX differences hurt the most:
for,readonly,maxlength,autocomplete,novalidate— they all need renaming. The converter gets all of them.Handle inline styles. If your legacy HTML uses inline styles for layout or one-off overrides, the converter translates all of them to JSX style objects. Then you can decide whether to keep them or extract to CSS modules.
Wire up events. The converter renames
onclicktoonClick,onsubmittoonSubmit, etc. You still need to point the handler at an actual function in your component — but the attribute is in the right format.Use JSX to HTML for the reverse. If any new React components need to be shared with a non-React context (email templates, legacy CMS pages, documentation), run them through the JSX→HTML converter to get clean HTML output.
Frequently Asked Questions
Does the converter send my code anywhere?
No. Both converters run entirely in your browser using JavaScript string processing. No server receives your code. No file is uploaded. You can verify this in browser DevTools → Network tab — no requests will appear while converting.
Does it handle SVG attributes?
Yes. SVG inline attributes in JSX follow the same camelCase convention as HTML. stroke-width → strokeWidth, fill-opacity → fillOpacity, viewBox stays as-is.
What about data-* and aria-* attributes?
Both are kept unchanged. data-id, data-value, aria-label, aria-expanded are identical in HTML and JSX — no transformation needed.
Does it add the React import statement?
No — in React 17+ with the new JSX transform, you don't need import React from 'react' anymore. If you're on an older setup, add it manually.
Can I convert a full HTML page?
Technically yes, but <html>, <head>, and <body> tags are not a valid React component structure. Extract the content section (<main>, <article>, your component markup) and convert that.
Does it work with Tailwind CSS classes?
Yes. class="flex items-center gap-4 text-sm font-medium" becomes className="flex items-center gap-4 text-sm font-medium" — all class strings are passed through unchanged, only the attribute name changes.
Try It Now — Free, No Account, No Upload
Both tools are free, run entirely in your browser, and require no account or signup:
- HTML to JSX Converter — paste HTML, get React-ready JSX
- JSX to HTML Converter — paste JSX, get standard HTML
If you're in the middle of a React or Next.js migration, these will save you a significant amount of purely mechanical work. Paste your markup, convert, and move on to the parts that actually need thinking.
Found a conversion edge case that the tool doesn't handle? Drop it in the comments — the tricky cases are usually the most instructive ones.
Top comments (0)