
HTML to JSX: The Conversion That Trips Up Every React Beginner
π· Pexels / PexelsHTML to JSX: The Conversion That Trips Up Every React Beginner
Converting HTML to React JSX involves more than find-and-replace. Learn what changes, why it matters, and how to avoid the common mistakes.
There's a rite of passage that happens to almost every developer learning React. You've got a nice chunk of HTML β maybe from a design handoff, a static template, or something you copied from an old project β and you drop it straight into a component. Then the errors start.
Warning: Invalid DOM property `class`. Did you mean `className`?
Then another one.
Warning: Invalid DOM property `for`. Did you mean `htmlFor`?
Then possibly a parse error from an unclosed <img> or <input> tag. By the time you've scrolled through six warnings, you're wondering why React couldn't just accept regular HTML. Fair question, actually.
Why JSX Isn't Just HTML
The short answer: JSX looks like HTML but it's JavaScript in disguise. When Babel processes your component, every <div className="wrapper"> becomes React.createElement('div', { className: 'wrapper' }). That transformation is what makes JSX so powerful β but it's also why you can't copy HTML verbatim.
HTML is a markup language. It has its own parsing rules, and browsers are designed to be forgiving about it. JSX is JavaScript syntax that happens to resemble HTML. JavaScript parsers are not forgiving. They have reserved keywords, strict syntax rules, and no tolerance for ambiguity.
That tension β HTML's looseness versus JavaScript's strictness β is the root cause of every conversion error you'll encounter.
The Transformations You Need to Know
1. class β className
This is the one everyone learns first, usually from a warning.
<!-- HTML -->
<div class="container main-content">
<p class="text-gray">Hello world</p>
</div>
// JSX
<div className="container main-content">
<p className="text-gray">Hello world</p>
</div>
The reason is straightforward: class is a reserved keyword in JavaScript (it's how you define ES6 classes). If JSX allowed class as an attribute, the JavaScript parser would get confused. className is a thin workaround that maps directly to the DOM property of the same name β when React renders to the real DOM, it sets the className property, which corresponds to the HTML class attribute. The end result is identical; only the syntax changes.
2. for β htmlFor
Same logic. for is a reserved keyword in JavaScript (it's how you write for loops). Labels in forms use for to connect to an input's id:
<!-- HTML -->
<label for="email">Email address</label>
<input type="email" id="email" />
// JSX
<label htmlFor="email">Email address</label>
<input type="email" id="email" />
A lot of developers remember className but forget htmlFor. It's less commonly discussed but will throw the same kind of warning if you miss it.
3. Self-closing void elements
In HTML, void elements β tags that can't have children β don't need a closing slash. Browsers are fine with this:
<!-- Valid HTML -->
<img src="photo.jpg" alt="A photo">
<input type="text" name="username">
<br>
<hr>
<meta charset="UTF-8">
JSX requires every element to be explicitly closed. You either add the closing slash before the >, or you add a separate closing tag:
// JSX - must self-close
<img src="photo.jpg" alt="A photo" />
<input type="text" name="username" />
<br />
<hr />
<meta charSet="UTF-8" />
Note I also changed charset to charSet in that last example. That's the camelCase pattern, which we'll get to next.
4. CamelCase attributes
HTML attributes are case-insensitive. JSX attribute names map to DOM properties, and DOM properties follow camelCase conventions. The most common ones you'll hit:
| HTML | JSX |
|---|---|
onclick | onClick |
onchange | onChange |
tabindex | tabIndex |
maxlength | maxLength |
readonly | readOnly |
autocomplete | autoComplete |
autofocus | autoFocus |
crossorigin | crossOrigin |
charset | charSet |
Event handlers are probably the most frequently encountered. HTML event attributes use all-lowercase:
<!-- HTML -->
<button onclick="handleClick()">Click me</button>
<input onchange="handleChange(event)" />
In JSX, they're camelCase and you pass a function reference (not a string):
// JSX
<button onClick={handleClick}>Click me</button>
<input onChange={handleChange} />
That last point β passing a function reference rather than a string call β is an important difference. In HTML onclick, the value is a string of JavaScript to execute. In JSX onClick, the value is a JavaScript expression (usually a function reference). That's why JSX uses curly braces {} around the value instead of quotes.
5. Inline styles as JavaScript objects
This one surprises people the first time. HTML inline styles are strings:
<!-- HTML -->
<div style="color: red; font-size: 16px; margin-top: 8px;">
Styled text
</div>
JSX inline styles are JavaScript objects, which means:
- Double curly braces (one pair for the JSX expression, one for the object literal)
- Property names in camelCase
- Values as strings or numbers (numbers for pixel values can optionally be plain numbers)
// JSX
<div style={{ color: 'red', fontSize: '16px', marginTop: '8px' }}>
Styled text
</div>
The camelCase rule applies here too: font-size becomes fontSize, margin-top becomes marginTop, background-color becomes backgroundColor. Any hyphenated CSS property becomes camelCase.
6. Comments
In HTML you write <!-- comment -->. In JSX, HTML-style comments don't work. You need to use JavaScript comments inside a JSX expression:
<!-- HTML comment -->
<div>
<!-- This is a comment -->
<p>Content</p>
</div>
{/* JSX comment */}
<div>
{/* This is a comment */}
<p>Content</p>
</div>
You can also use // for single-line comments, but only inside the curly braces or in regular JavaScript code β not directly in JSX markup.
Less Obvious Gotchas
Boolean attributes behave differently
In HTML, the presence of an attribute implies true for boolean attributes:
<!-- HTML - these are equivalent -->
<input disabled>
<input disabled="true">
<input disabled="disabled">
In JSX, boolean attributes can be written shorthand (like HTML), but the explicit form looks different:
// JSX - these are all equivalent
<input disabled />
<input disabled={true} />
What you should avoid in JSX is disabled="true" (with quotes), because that passes the string "true" rather than the boolean true. React might render it correctly, but it's not idiomatic and can cause issues with some components.
The key prop in lists
When you convert HTML lists to JSX and map over data, React needs a key prop on each element. This isn't an HTML-to-JSX conversion issue per se β it only applies when you're generating elements dynamically β but it's worth knowing before you hit the warning:
// React will warn without key
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
SVG attributes
SVG has its own set of attribute naming quirks. In HTML/SVG, you might write fill-opacity, stroke-width, clip-path. In JSX these become fillOpacity, strokeWidth, clipPath. If you're pasting in inline SVG from an icon library or design tool, expect to do some cleanup.
tabindex vs tabIndex
Easy to miss. If you're building accessible components and setting tabindex manually, remember it becomes tabIndex in JSX. Missing this won't always throw an error β React may still render it β but it's a silent difference that could affect behavior.
When to Use an Automated Converter
If you have a large block of HTML β a component from a design system, a template you're migrating from a non-React codebase, or a snippet from an online source β running it through a converter is the practical choice. Manually replacing every class with className and every hyphenated event handler is tedious and error-prone.
The ToolPal HTML to JSX converter handles the standard transformations automatically: class β className, for β htmlFor, style strings β style objects, self-closing void elements, camelCase attributes. You paste your HTML, get back valid JSX, and move on.
Where automated converters can't help is with the structural decisions. Converting 500 lines of HTML into a single JSX return statement doesn't give you a well-architected component β it gives you a converted blob. For anything substantial, the converter handles the syntax but you still need to decide how to break the result into smaller components, where props should replace hardcoded values, and which parts should be dynamic.
Use the converter for the mechanical transformation. Use your brain for the architecture.
When to Write JSX From Scratch
If you're building a new component and you have the design in your head (or in a mockup), writing JSX directly is often faster than writing HTML and converting it. You'll write className from the start, you won't forget to close your void elements, and you won't end up with style strings that need converting.
The cases where conversion genuinely helps:
Migrating from a non-React project. If you have existing HTML templates β from a plain HTML/CSS site, a Rails app, or a Jinja template β conversion tools save real time.
Working from a design handoff. Designers often export HTML markup or provide snippets. Running that through a converter is faster than retyping it.
Pasting in third-party snippets. Code examples from documentation, Stack Overflow answers, or tutorials are often written in HTML. A quick conversion gets you to usable JSX without manual editing.
Large forms. Forms with dozens of inputs, labels, and fieldsets are tedious to write by hand. Paste the HTML structure, convert it, then replace the hardcoded values with props and state.
A Realistic Conversion Example
Here's a small card component in HTML:
<div class="card" id="product-card">
<img src="/product.jpg" alt="Product photo" class="card-image">
<div class="card-body">
<h2 class="card-title">Product Name</h2>
<p class="card-description" style="color: #666; font-size: 14px;">
A short description of the product.
</p>
<label for="quantity">Quantity</label>
<input type="number" id="quantity" min="1" max="99" readonly>
<button class="btn btn-primary" onclick="addToCart()">Add to Cart</button>
</div>
</div>
After conversion:
<div className="card" id="product-card">
<img src="/product.jpg" alt="Product photo" className="card-image" />
<div className="card-body">
<h2 className="card-title">Product Name</h2>
<p className="card-description" style={{ color: '#666', fontSize: '14px' }}>
A short description of the product.
</p>
<label htmlFor="quantity">Quantity</label>
<input type="number" id="quantity" min="1" max="99" readOnly />
<button className="btn btn-primary" onClick={addToCart}>Add to Cart</button>
</div>
</div>
Notice every change:
- All
classβclassName <img>and<input>are self-closed with/>stylestring β style object with camelCase propertiesforβhtmlForreadonlyβreadOnlyonclick="addToCart()"βonClick={addToCart}(function reference, no parentheses)
That last point about the event handler is worth stressing. In the HTML version, onclick="addToCart()" is a string that gets evaluated. If you accidentally write onClick="addToCart()" in JSX, React will throw an error about event handlers needing to be functions, not strings. And if you write onClick={addToCart()} (with parentheses), you'll call the function immediately on render instead of on click. Both are common beginner mistakes.
A Few Things Converters Can't Handle
Dynamic content. A converter doesn't know that Product Name should become {product.name}. It'll convert the structure but you'll need to replace static values with props and state yourself.
Event handler logic. onclick="addToCart()" will convert to onClick={addToCart}, but addToCart still needs to be defined somewhere in your component. The converter gives you the attribute syntax; the function is your responsibility.
Conditional rendering. HTML doesn't have a concept of "show this element only if X is true." That pattern is something you add during the componentization step, not something a converter can infer from static markup.
Multiple root elements. If your HTML snippet has two sibling elements at the top level, the converted JSX will also have two sibling elements β which is invalid in a component return statement. You'll need to wrap them in a <div> or <>...</> fragment.
HTML-to-JSX conversion is one of those things that feels annoying until it becomes second nature. After a few weeks of writing React, you'll start typing className automatically without thinking. You'll close your <img /> tags reflexively. But until that muscle memory develops, tools like the ToolPal HTML to JSX converter make the transition faster and less frustrating.
The real skill isn't memorizing the syntax differences β it's understanding why they exist. Once you understand that JSX is JavaScript, the rules stop feeling arbitrary and start making sense.