DeveloperApril 2026 · 14 min read

How to Convert HTML to JSX: The Complete Developer's Guide

Convert HTML markup to valid React JSX — the attribute renames, the inline-style object transform, the self-closing void elements, and the few cases where the converter hands the wiring back to you.

⚛
Try the HTML to JSX Converter
Free, no signup
→
DG
Derek Giordano
Designer & Developer
In this guide
01Why HTML Isn't JSX02Attribute Renames (The Big List)03Event Handlers: camelCase and Functions04Self-Closing Void Elements05Inline Styles: String to Object06HTML Comments to JSX Comments07Boolean Attributes08Wrapping the Output as a Component09Edge Cases and Gotchas10Common Workflows11HTML vs JSX: Quick Reference12What to Do After Converting13Frequently Asked Questions
⚡ Key Takeaways
  • Convert HTML to React JSX.
  • Why HTML Isn't JSX.
  • Covers attribute renames (the big list).
  • Covers event handlers: camelcase and functions.
  • Covers self-closing void elements.

Why HTML Isn't JSX

If you've ever pasted a snippet of HTML into a React component expecting it to just work, you've seen the problem. The browser console lights up with warnings — Invalid DOM property 'class', Unknown event handler property 'onclick', Inline style should be a JavaScript object — and half your markup silently stops behaving the way it did on a plain HTML page.

JSX *looks* like HTML, which is the whole point. React's designers wanted markup to feel familiar. But JSX is JavaScript, not HTML, and JavaScript comes with its own rules: class is a reserved keyword, event handlers are functions not strings, and CSS inline styles are objects because you need to compose them at runtime.

The HTML to JSX Converter runs through the mechanical fixups in one paste: attribute renames (class → className, for → htmlFor, 30+ others), event handlers to camelCase, void elements self-closed, inline styles parsed into JavaScript objects, and HTML comments converted to the {/* */} JSX form. Optionally it wraps the whole thing in a default-exported functional component with a name you choose.

This guide walks through what the converter handles, the specific rename rules, the style-object conversion, and the few edge cases — SVG namespaces, boolean attributes, inline scripts — where you'll still want to look at the output before committing.

Attribute Renames (The Big List)

The single largest category of HTML-to-JSX changes is attributes that got renamed because their HTML names conflict with JavaScript reserved words or don't match JavaScript's camelCase convention. Here's the full list the converter knows about:

💡 Tip
Use 3+ color stops instead of 2 to avoid the muddy gray band that appears in the center of complementary-color gradients.

The two most common — you'll see these on every paste:

class → className for → htmlFor

class is a reserved word in JavaScript (introduced with ES6's class syntax). for is the for loop keyword. React uses className and htmlFor to sidestep both.

Attributes that became camelCase:

tabindex → tabIndex readonly → readOnly maxlength → maxLength minlength → minLength cellspacing → cellSpacing cellpadding → cellPadding rowspan → rowSpan colspan → colSpan usemap → useMap frameborder → frameBorder contenteditable → contentEditable crossorigin → crossOrigin datetime → dateTime enctype → encType formaction → formAction formenctype → formEncType formmethod → formMethod formnovalidate → formNoValidate formtarget → formTarget hreflang → hrefLang inputmode → inputMode novalidate → noValidate radiogroup → radioGroup spellcheck → spellCheck srcdoc → srcDoc srclang → srcLang srcset → srcSet autocomplete → autoComplete autofocus → autoFocus autoplay → autoPlay playsinline → playsInline

Hyphenated attributes that become camelCase:

accept-charset → acceptCharset http-equiv → httpEquiv
**Note on data-* and aria-*.** These stay hyphenated in JSX — React treats them specially. data-testid, aria-label, aria-describedby all work as-is without renames. The converter leaves them alone.

The conversion is case-insensitive, so ONCLICK, OnClick, and onclick all become onClick. The pattern uses word boundaries, so a string like class="noclass" inside a value (not an attribute name) won't get touched.

Event Handlers: camelCase and Functions

HTML event handlers are strings. JSX event handlers are functions. The converter handles the naming part; you have to handle the value part.

⚠ Warning
CSS gradients used as backgrounds cannot be animated with standard transitions. Use background-size animation or @property registered custom properties instead.

What the converter does. Every on<something>= attribute gets capitalized:

onclick → onClick onmouseover → onMouseOver onkeydown → onKeyDown onchange → onChange onsubmit → onSubmit onfocus → onFocus onblur → onBlur ontouchstart → onTouchStart

The pattern is simple: on, then the event name with its first letter capitalized. The regex handles any on* attribute, so less common events (onpointerdown, onwheel, ontransitionend) all get the same treatment.

What the converter does NOT do. The value stays a string. So this:

<button onclick="submit()">Go</button>

Becomes this:

<button onClick="submit()">Go</button>

Which *looks* right but isn't — React expects a function reference, not a string. You'll get a console warning (Expected onClick listener to be a function, instead got a value of 'string' type), and the handler won't fire. To fix it:

<button onClick={handleSubmit}>Go</button>

Or inline:

<button onClick={() => submit()}>Go</button>

This is deliberate — the converter can't invent a function that doesn't exist, and silently generating something like onClick={() => eval("submit()")} would be worse than leaving the wiring for you.

Self-Closing Void Elements

Void elements are HTML tags that can't have children — <img>, <input>,
, <hr>, <meta>, <link>, <area>, <base>, <col>, <embed>, <param>, <source>, <track>, <wbr>. In HTML you can write <img src="x"> without a closing tag and the parser knows what to do. In JSX, you can't — all tags must either have a matching close tag or be self-closed with />.

The converter handles this automatically. Every void element gets the /> suffix added:

<img src="/logo.png" alt="Logo"> // becomes <img src="/logo.png" alt="Logo" /> <input type="email" required> // becomes <input type="email" required={true} /> <br> // becomes <br />

The regex is clever about not double-closing things that are already self-closed — so <img src="x" /> stays as <img src="x" />, it doesn't become <img src="x" / />. The lookbehind (?<!\/) in the pattern skips anything already ending in /.

Why JSX requires this. JSX compiles to React.createElement(tag, props, ...children) calls. Without a close tag or self-close marker, the parser has no way to know where the element ends. <img src="x">next could mean "img followed by span" or "img with span as a child." Self-closing removes the ambiguity.

Inline Styles: String to Object

This is the most transformative conversion the tool does, and the one that saves the most typing. In HTML:

<div style="color: red; font-size: 16px; margin-top: 8px;">Hi</div>

In JSX:

<div style={{color: "red", fontSize: "16px", marginTop: "8px"}}>Hi</div>

The converter splits the style string on semicolons, parses each property: value pair, camelCases the property name (kebab-case → camelCase), and wraps everything in a double-braced JSX expression — the outer {} is the JSX expression delimiter, the inner {} is the JS object literal.

Rules the converter follows:

A subtle point: values stay strings. React accepts both strings and numbers for most properties, but the converter always emits strings. For properties that take unitless numbers (like zIndex, opacity, flexGrow), you can manually remove the quotes after converting — but it's not required for correctness.

What doesn't convert. The converter only handles inline styles. Stylesheets in <style> blocks or external CSS files stay as they are — which is usually what you want, since those don't have the JSX-object issue in the first place.

HTML Comments to JSX Comments

HTML comments look like <!-- this is a comment -->. In JSX, <!-- isn't a recognized syntax — the parser treats < as an element opener and fails. You either have to delete the comments or convert them to the JSX form.

The converter takes the second path:

<!-- Form section --> // becomes {/* Form section */}

The wrapping {} is the JSX expression delimiter; /* */ is standard JS block comment syntax. The result is a valid JSX expression that evaluates to undefined at runtime, which React renders as nothing.

Multi-line comments work the same way:

<!-- TODO: refactor this into a proper component --> // becomes {/* TODO: refactor this into a proper component */}
If the comment itself contains the characters */, the JS parser will end the comment early and probably throw a syntax error. This is rare in practice but worth knowing if you're converting machine-generated HTML that might have nested comments.

Boolean Attributes

In HTML, certain attributes are "boolean" — just their presence means true. <input disabled> means the input is disabled; there's no value after the attribute. You can also write <input disabled="disabled"> or <input disabled=""> — all three are equivalent.

In JSX, the bare-attribute form works for some attributes but is fragile. The converter makes the assignment explicit:

<input type="email" required> // becomes <input type="email" required={true} /> <option selected>Default</option> // becomes <option selected={true}>Default</option> <details open> // becomes <details open={true}>

The attributes the converter treats as boolean: checked, disabled, required, readOnly, hidden, selected, multiple, default, reversed, open, autoFocus, autoPlay, controls, loop, muted, playsInline.

Why ={true} instead of just the attribute name? Because in real React code, these values usually come from state or props, not hardcoded. Explicit ={something} is the natural shape to edit:

<input type="email" required={isRequired} /> <button disabled={!formValid}>Submit</button>

So the converter generates the pattern you'll actually type in half a second, rather than the terser form you'd have to expand anyway.

Wrapping the Output as a Component

The converter has a "Wrap as component" toggle. When enabled, the output is the raw JSX wrapped inside a complete functional component with a default export:

export default function MyComponent() { return ( <> <div className="container"> <h1>Hello</h1> <p>Welcome</p> </div> </> ); }

The <>...</> is a React Fragment — it lets the component return multiple top-level elements without wrapping them in an extra <div>. You can rename the component using the text input next to the toggle (only alphanumeric characters are accepted, so My-Component gets sanitized to MyComponent).

When to use Wrap mode:

When to leave it off:

Edge Cases and Gotchas

The places where the converter's output needs a second look before it's production-ready.

Inline <script> tags. Scripts inside HTML stay as literal text in the JSX output. React will render them as <script> elements, but they won't execute — React strips inline scripts on render for security reasons. If your pasted HTML has an important inline script, the logic needs to move into a component effect or be loaded via a src= attribute.

SVG with xmlns:xlink. The xmlns:xlink attribute gets partially handled (namespace attributes are a specific category). Modern SVGs mostly don't use xlink:href anymore — they use plain href — so this is rarely a problem. If you're converting an older SVG icon library, spot-check the output.

Style property with CSS custom properties. A declaration like style="--brand: #FF6B6B; color: var(--brand);" converts correctly (the --brand key is preserved, the var() reference stays as a string value). But if you're passing variables from component props, you'll need to switch to template literals or the spread syntax manually.

Multiple root elements in a snippet. If your pasted HTML has multiple top-level elements (like two <div> siblings), the converter produces valid JSX for each one — but the wrapped-component mode uses a <> fragment so both get returned. Without wrap mode, you'll need to wrap the output yourself in something (a fragment, a <div>, or array syntax) before React accepts it.

Character entities in text nodes. HTML entities like &, <,   are preserved as-is in the output. JSX understands these in text content, so they render correctly. But inside attribute values, they get tricky — & inside a href works fine, but inside a JS expression it won't. Spot-check hrefs and similar.

The overall pattern: the converter is mechanically correct for the common cases and explicit about handing off to you for the cases that require judgment (functions for event handlers, scripts, dynamic values). It's not trying to be a code-generating AI; it's trying to be a reliable shell that handles the tedious 95%.

Common Workflows

Three realistic ways developers use this tool.

1. Figma / design-tool export to component. Many design tools now export HTML/CSS snippets for handoff. These are almost always HTML (not JSX), even in 2026. Paste the export, enable "Wrap as component", name it after the design frame, copy, and drop into your components/ directory. Clean up the inline styles by extracting to a stylesheet or Tailwind classes afterward.

2. Moving a legacy HTML page into React. Converting an existing static site to React one page at a time? Paste the <body> contents section by section. The converter handles the attribute renames and style syntax; you'll still need to split the markup into logical components, replace string event handlers with real functions, and wire up any forms to controlled inputs. But the boring 60% of the conversion — renaming 200 class attributes — is done in a paste.

3. Email templates and marketing HTML. Email HTML is a nightmare — inline styles everywhere, tables for layout, vendor-specific <!--[if mso]> conditional comments. The converter handles the inline-style conversion cleanly. The conditional comments become JSX comments (which is fine — they just won't be rendered). The tables-for-layout stay as tables, which is still correct for email-like contexts.

The pattern. Paste → pick toggle (wrap or not) → copy → drop into your file → fix the things the converter explicitly leaves for you (event handler functions, dynamic values, script logic). What you save is the mechanical part, which is exactly the part that's soul-crushing to do manually at any scale.

HTML vs JSX: Quick Reference

A cheat sheet you can bookmark. Every difference between HTML and JSX, with the converter's output in the middle.

HTML JSX ----------------------------------------- class="box" className="box" for="email" htmlFor="email" tabindex="0" tabIndex="0" readonly readOnly={true} onclick="fn()" onClick="fn()" (needs fn ref) onkeydown="k(e)" onKeyDown="k(e)" (needs fn ref) style="color: red" style={{color: "red"}} style="--brand: #F00" style={{"--brand": "#F00"}} <img src="x"> <img src="x" /> <br> <br /> <input disabled> <input disabled={true} /> <!-- comment --> {/* comment */}   in text   in text (works) & in text & in text (works) <script>... <script>... (won't execute)

The lines marked "needs fn ref" are the places the converter can't help you — where the string value of an attribute needs to become a real JavaScript function. Everything else is mechanical.

What to Do After Converting

The converter handles the syntax. Code quality is your job. Here's the checklist I run through before committing JSX converted from HTML.

Replace string event handlers with functions. Search the output for patterns like onClick=" — any quote-wrapped value on an on* attribute needs to become a function reference.

Move inline styles to stylesheets. Inline styles in JSX are valid, but they make components harder to theme, harder to reuse, and harder to optimize. For one-off values (like a dynamic width from a prop), keep them inline. For anything reused, move to CSS/SCSS/Tailwind classes. If you're on Tailwind, the CSS to Tailwind converter handles the next step.

Check form inputs. HTML forms using <input> with native attributes (required, pattern, native validation) work in React but feel weird — most React codebases use controlled inputs with explicit state. Decide whether to keep uncontrolled (simpler, native) or switch to controlled (more React-idiomatic, more flexible).

Extract reusable pieces. A converted HTML page is one giant component. Look for repeated patterns — cards, list items, form rows — and break them into their own components that take props. This is the point where you get the React-architecture benefits.

Accessibility pass. The converter preserves your original HTML's accessibility — which is good if the original was accessible and a problem if it wasn't. Check <img> tags have alt text. Check form inputs have labels. Check focus states work. React's tooling (like eslint-plugin-jsx-a11y) can catch most issues automatically once the JSX is in a real project.

Frequently Asked Questions

Can I paste an entire web page's HTML?

Yes, but with caveats. The converter handles the syntax conversion for any amount of HTML. What doesn't translate cleanly: inline script tags (they won't execute in React), DOCTYPE declarations (not valid JSX), html, head, and body tags (these are owned by the page shell in a React app, not a component). For a full-page conversion, paste the body contents only — that gives you the component-shaped output.

What about React components that use TypeScript?

The output is plain JSX, which is valid in both .jsx and .tsx files. If you're using TypeScript, the component will work immediately but won't have types on its props (since the converter isn't inferring a props interface from nothing). When you enable Wrap as component, the exported component takes no props — add a typed props parameter after you paste the output into your .tsx file.

Does it handle SVG markup?

Mostly yes. Simple SVG icons — the kind you'd copy from an icon library — convert cleanly: class becomes className, attributes like stroke-width become strokeWidth, and the output is valid JSX you can drop into a component. Edge cases: namespaced attributes like xmlns:xlink and xlink:href may need manual adjustment for very complex SVGs, and script tags inside SVG (used by some interactive SVGs) won't execute in React.

Why does the converter add ={true} to boolean attributes?

Because that's the form you'll actually edit. In real React code, boolean attributes almost always take a dynamic value — disabled={isLoading}, required={fieldRequired}, hidden={!shouldShow}. Generating the explicit ={true} form gives you the exact shape to swap in a state variable or prop. The terser bare-attribute form is legal JSX but fragile and less common in modern codebases.

What happens to my HTML if it has nested quotes?

The converter handles both single and double-quoted attribute values. If your HTML has something like

(single-quoted), the regex matches both quote styles. For edge cases with escaped quotes inside JSON-ish attribute values, review the output — JSX doesn't have the same quote-escaping rules as HTML, so a pattern that was valid HTML might need curly-brace expression syntax in JSX: title={"She said hi"}.

Can I use this tool offline or for enterprise HTML?

The entire tool runs in your browser — once the page has loaded, it works without a network connection. No paste of yours ever leaves your device. This is intentional because HTML conversions often include proprietary markup, design system code, and marketing copy that your employer doesn't want on someone else's server. For enterprise use, you can save the page locally (browser File > Save As) and use the saved copy as a local offline tool.

Try it yourself

Use the HTML to JSX Converter — free, no signup required.

⚛ Open HTML to JSX Converter
DG
Derek Giordano
Written by the creator of Ultimate Design Tools. BA in Business Marketing.