How to Convert HTML to JSX for React (Complete Guide)
This guide has a free tool → Open HTML to JSX Converter
How to Convert HTML to JSX for React (Complete Guide)
JSX looks like HTML - but it is not. It is a syntax extension for JavaScript that the React compiler transforms into React.createElement() calls. Because JSX compiles to JavaScript, it follows JavaScript rules, not HTML rules. This distinction creates a set of precise differences that trip up every developer who copies HTML into a React component for the first time.
This guide covers every HTML-to-JSX conversion rule, the most common mistakes, complete worked examples of real-world HTML (navigation, forms, cards, SVGs), and how to handle edge cases like inline styles, data attributes, and accessibility markup.
HTML to JSX Converter
Free online HTML to JSX converter - convert HTML markup to valid React JSX with automatic attribute and style transformations
Code Formatter
Free online code formatter - beautify and format JavaScript, CSS, HTML, and more
Why HTML and JSX Are Different
When you write:
<div className="container">Hello</div>The JSX transformer (Babel, SWC, or the TypeScript compiler) converts this to:
React.createElement("div", { className: "container" }, "Hello");This is pure JavaScript. The property names you pass to React.createElement must be valid JavaScript identifiers, which is why class (a reserved keyword) cannot be used - it becomes className. Understanding this compilation step makes all the conversion rules logical rather than arbitrary.
Complete Rule Reference: Every HTML-to-JSX Conversion
Rule 1: class Becomes className
class is a reserved keyword in JavaScript (used for class declarations). JSX uses className instead.
<!-- HTML -->
<div class="container">
<p class="text-red font-bold">
<button class="btn btn-primary btn-lg">Submit</button>
<input class="form-control" type="text">// JSX
<div className="container">
<p className="text-red font-bold">
<button className="btn btn-primary btn-lg">Submit</button>
<input className="form-control" type="text" />This is the most frequent conversion you will make. In large HTML blocks, there can be dozens of these.
Rule 2: for Becomes htmlFor
for is a reserved JavaScript keyword (used in for loops and for...of). The JSX attribute becomes htmlFor.
<!-- HTML -->
<label for="username">Username</label>
<input id="username" type="text">
<label for="remember">
<input type="checkbox" id="remember"> Remember me
</label>// JSX
<label htmlFor="username">Username</label>
<input id="username" type="text" />
<label htmlFor="remember">
<input type="checkbox" id="remember" /> Remember me
</label>The id attribute stays the same - only for on <label> elements changes to htmlFor.
Rule 3: Inline Styles Take a JavaScript Object
In HTML, the style attribute takes a CSS string. In JSX, it takes a JavaScript object with camelCase property names.
<!-- HTML: CSS string with kebab-case properties -->
<div style="background-color: #f3f4f6; font-size: 16px; margin-top: 8px;">
<span style="color: red; font-weight: bold; text-transform: uppercase;">// JSX: JavaScript object with camelCase properties
<div style={{ backgroundColor: '#f3f4f6', fontSize: '16px', marginTop: '8px' }}>
<span style={{ color: 'red', fontWeight: 'bold', textTransform: 'uppercase' }}>The double curly braces are not a special JSX syntax - the outer {} is a JSX expression, and the inner {} is the JavaScript object literal.
Full camelCase conversion reference for common properties:
| CSS (kebab-case) | JSX (camelCase) |
|---|---|
background-color | backgroundColor |
font-size | fontSize |
font-weight | fontWeight |
font-family | fontFamily |
margin-top | marginTop |
margin-bottom | marginBottom |
margin-left | marginLeft |
margin-right | marginRight |
padding-top | paddingTop |
border-radius | borderRadius |
box-shadow | boxShadow |
text-transform | textTransform |
text-decoration | textDecoration |
text-align | textAlign |
line-height | lineHeight |
letter-spacing | letterSpacing |
z-index | zIndex |
flex-direction | flexDirection |
align-items | alignItems |
justify-content | justifyContent |
pointer-events | pointerEvents |
list-style-type | listStyleType |
white-space | whiteSpace |
word-break | wordBreak |
overflow-x | overflowX |
max-width | maxWidth |
min-height | minHeight |
Units still go in strings when the value has a unit: { fontSize: '16px' } not { fontSize: 16 }. The exception: z-index, opacity, and flex accept plain numbers in JSX.
Rule 4: Self-Closing Tags Are Required
HTML void elements do not need closing tags. JSX requires every element to be closed - either with a closing tag or a self-closing slash.
<!-- HTML: void elements without closing tags (valid) -->
<img src="photo.jpg" alt="My photo">
<br>
<input type="text" name="email">
<hr>
<link rel="stylesheet" href="styles.css">
<meta charset="UTF-8">
<area shape="rect" coords="0,0,100,100" href="page.html">
<base href="https://example.com">
<col span="2">
<embed src="video.mp4" type="video/mp4">
<param name="autoplay" value="true">
<source src="video.mp4" type="video/mp4">
<track src="subtitles.vtt">
<wbr>// JSX: all elements must be closed
<img src="photo.jpg" alt="My photo" />
<br />
<input type="text" name="email" />
<hr />
<link rel="stylesheet" href="styles.css" />
<meta charSet="UTF-8" />
<area shape="rect" coords="0,0,100,100" href="page.html" />
<base href="https://example.com" />
<col span={2} />
<embed src="video.mp4" type="video/mp4" />
<param name="autoplay" value="true" />
<source src="video.mp4" type="video/mp4" />
<track src="subtitles.vtt" />
<wbr />Note: charset on <meta> becomes charSet (camelCase) in JSX.
Non-void elements with children must use explicit closing tags:
// These must use closing tags:
<div className="container">content</div>
<p>paragraph text</p>
<button onClick={handleClick}>Click me</button>
<select>
<option value="a">Option A</option>
</select>Rule 5: Event Handlers Use camelCase
All HTML event attributes become camelCase in JSX and accept function references rather than inline script strings.
<!-- HTML: inline event handlers as strings -->
<button onclick="handleClick()">
<input onchange="handleChange(event)">
<form onsubmit="handleSubmit(event)">
<div onmouseover="handleHover()">
<a href="#" onclick="doSomething(); return false;">// JSX: camelCase event handlers as function references
<button onClick={handleClick}>
<input onChange={handleChange}>
<form onSubmit={handleSubmit}>
<div onMouseOver={handleHover}>
<a href="#" onClick={(e) => { e.preventDefault(); doSomething(); }}>Full event handler conversion table:
| HTML attribute | JSX attribute |
|---|---|
onclick | onClick |
onchange | onChange |
onsubmit | onSubmit |
onkeydown | onKeyDown |
onkeyup | onKeyUp |
onkeypress | onKeyPress |
onmouseover | onMouseOver |
onmouseout | onMouseOut |
onmouseenter | onMouseEnter |
onmouseleave | onMouseLeave |
onmousemove | onMouseMove |
onmousedown | onMouseDown |
onmouseup | onMouseUp |
onfocus | onFocus |
onblur | onBlur |
oninput | onInput |
onload | onLoad |
onerror | onError |
onscroll | onScroll |
ontouchstart | onTouchStart |
ontouchmove | onTouchMove |
ontouchend | onTouchEnd |
ondragstart | onDragStart |
ondrop | onDrop |
ondragover | onDragOver |
oncontextmenu | onContextMenu |
oncopy | onCopy |
oncut | onCut |
onpaste | onPaste |
Rule 6: Comments Use JSX Syntax
HTML comments (<!-- -->) are not valid JSX. Use JavaScript block comments wrapped in curly braces.
<!-- HTML comment - not valid in JSX -->
<div>
<!-- This is a section header -->
<h2>Title</h2>
</div>// JSX comment syntax
<div>
{/* This is a section header */}
<h2>Title</h2>
</div>JavaScript line comments inside JSX markup are also possible but rarely used:
<div
className="container" // Applied to the outer div
id="main"
>Rule 7: Boolean Attributes
In HTML, the presence of a boolean attribute implies true. In JSX, boolean attributes work similarly - you can write them without a value (which defaults to true) or be explicit.
<!-- HTML: boolean attributes with no value -->
<input disabled>
<input readonly>
<select multiple>
<details open>
<input checked>
<option selected>
<video autoplay muted loop>
<input required>// JSX: two equivalent ways to write boolean attributes
<input disabled /> {/* Same as disabled={true} */}
<input disabled={true} /> {/* Explicit */}
<input disabled={false} /> {/* This REMOVES the attribute */}
<input readOnly /> {/* Note: readonly becomes readOnly */}
<select multiple />
<details open />
<input defaultChecked /> {/* Note: checked => defaultChecked for uncontrolled */}
<option selected /> {/* Note: selected => defaultValue on <select> for controlled */}
<video autoPlay muted loop /> {/* Note: autoplay becomes autoPlay */}
<input required />Important distinctions:
readonly→readOnlyautoplay→autoPlaytabindex→tabIndexaccesskey→accessKeycrossorigin→crossOriginenctype→encTypeusemap→useMap
Rule 8: data-* and aria-* Attributes Stay the Same
These are the exceptions to camelCase. Data attributes and ARIA attributes keep their hyphenated format in JSX.
<!-- HTML -->
<div data-testid="sidebar" data-user-id="42">
<button aria-label="Close" aria-expanded="false" aria-controls="menu">
<input aria-describedby="email-hint" aria-required="true">// JSX - no changes needed for data-* and aria-*
<div data-testid="sidebar" data-user-id="42">
<button aria-label="Close" aria-expanded={false} aria-controls="menu">
<input aria-describedby="email-hint" aria-required={true} />Note: while aria-expanded and aria-required accept string values in HTML ("false", "true"), React accepts and recommends boolean values for these when appropriate: aria-expanded={false}.
Rule 9: SVG Attribute Conversion
SVG attributes have their own conversion rules, separate from HTML. Many SVG attributes use camelCase in JSX where HTML SVG attributes use lowercase or hyphenated forms.
<!-- SVG in HTML -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" fill="none">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
<circle cx="12" cy="12" r="5" fill-opacity="0.5" fill-rule="evenodd" />
<clipPath id="clip">
<rect x="0" y="0" width="24" height="24" />
</clipPath>
</svg>// SVG in JSX
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" strokeWidth={2} fill="none">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
<circle cx={12} cy={12} r={5} fillOpacity={0.5} fillRule="evenodd" />
<clipPath id="clip">
<rect x={0} y={0} width={24} height={24} />
</clipPath>
</svg>Common SVG attribute conversions:
| SVG HTML attribute | JSX attribute |
|---|---|
stroke-width | strokeWidth |
stroke-linecap | strokeLinecap |
stroke-linejoin | strokeLinejoin |
stroke-dasharray | strokeDasharray |
fill-opacity | fillOpacity |
fill-rule | fillRule |
clip-path | clipPath |
clip-rule | clipRule |
stop-color | stopColor |
stop-opacity | stopOpacity |
text-anchor | textAnchor |
font-size | fontSize |
font-family | fontFamily |
xlink:href | href (xlink deprecated) |
xmlns:xlink | (omit - not needed) |
xml:space | xmlSpace |
gradientUnits | gradientUnits (already camelCase) |
viewBox | viewBox (already camelCase) |
Pasting SVG from design tools like Figma or Illustrator into JSX is where SVG conversion issues appear most frequently. A tool that handles this automatically saves significant time.
Common Conversion Mistakes
Mistake 1: Multiple Root Elements
JSX expressions must have a single root element. This is because React.createElement returns a single object - you cannot return two objects from a function.
// Error: Adjacent JSX elements must be wrapped in an enclosing tag
return (
<h1>Title</h1>
<p>Paragraph</p>
);
// Fix 1: Wrap in a div (adds an element to the DOM)
return (
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
// Fix 2: Use a Fragment (no extra DOM element)
return (
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
);
// Fix 3: Explicit Fragment (needed when you require a key prop)
return (
<React.Fragment>
<h1>Title</h1>
<p>Paragraph</p>
</React.Fragment>
);Mistake 2: HTML Entities
Most HTML entities work in JSX, but some cause issues. The safest approach is to use the actual Unicode character or a JavaScript string.
// These work fine in JSX
<p>Price: €50 — limited time © 2026</p>
// When in doubt, use Unicode escapes or actual characters
<p>Price: {'\u20AC'}50 {'\u2014'} limited time</p>
<p>Price: {'€'}50 {'—'} limited time</p>
// Common entities and their Unicode equivalents
// → {'\u00A0'} or {' '} (non-breaking space)
// — → {'\u2014'} or {'—'}
// – → {'\u2013'} or {'–'}
// « → {'\u00AB'} or {'«'}
// » → {'\u00BB'} or {'»'}
// © → {'\u00A9'} or {'©'}
// ® → {'\u00AE'} or {'®'}
// ™ → {'\u2122'} or {'™'}
// • → {'\u2022'} or {'•'}
// … → {'\u2026'} or {'…'}Mistake 3: JavaScript Reserved Words in Attributes
Besides class and for, other HTML attributes conflict with JavaScript:
<!-- HTML attributes that change in JSX -->
<input type="text"> → no change
<label for="field"> → htmlFor="field"
<div class="container"> → className="container"
<input tabindex="0"> → tabIndex={0}
<input accesskey="n"> → accessKey="n"
<video autoplay> → autoPlay
<textarea readonly> → readOnly
<input maxlength="100"> → maxLength={100}
<input minlength="3"> → minLength={3}
<meta charset="UTF-8"> → charSet="UTF-8"
<td colspan="2"> → colSpan={2}
<td rowspan="3"> → rowSpan={3}Mistake 4: Mixing Controlled and Uncontrolled Patterns
In HTML, value, checked, and selected are initial state attributes. In React, using them creates controlled components where React manages the value. For uncontrolled components, use defaultValue, defaultChecked.
<!-- HTML: initial value for a text input -->
<input type="text" value="initial value">
<input type="checkbox" checked>
<option value="b" selected>Option B</option>// JSX: controlled component (React manages state)
const [value, setValue] = useState('initial value');
<input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
// JSX: uncontrolled component (DOM manages state)
<input type="text" defaultValue="initial value" />
<input type="checkbox" defaultChecked />
<select defaultValue="b">
<option value="a">Option A</option>
<option value="b">Option B</option>
</select>Mistake 5: Style Values Without Units
CSS properties that accept pixel values need explicit units in JSX inline styles:
// Wrong: missing units
<div style={{ width: 200, height: 100 }}>
// Correct: explicit px units
<div style={{ width: '200px', height: '100px' }}>
// Exception: unitless values (opacity, flex, z-index, line-height ratio)
<div style={{ opacity: 0.5, zIndex: 100, flex: 1, lineHeight: 1.5 }}>Note that lineHeight accepts either '1.5' (with unit: 1.5rem) or 1.5 (without unit: ratio). Using a ratio (number) is preferred for accessibility.
Complete Worked Examples
Converting a Navigation Bar
<!-- HTML -->
<nav class="navbar">
<a class="navbar-brand" href="/">MyApp</a>
<ul class="nav-list">
<li class="nav-item">
<a class="nav-link active" href="/dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings">Settings</a>
</li>
<li class="nav-item">
<button class="btn btn-sm btn-outline" onclick="logout()">Logout</button>
</li>
</ul>
</nav>// JSX
function Navbar({ onLogout }) {
return (
<nav className="navbar">
<a className="navbar-brand" href="/">MyApp</a>
<ul className="nav-list">
<li className="nav-item">
<a className="nav-link active" href="/dashboard">Dashboard</a>
</li>
<li className="nav-item">
<a className="nav-link" href="/settings">Settings</a>
</li>
<li className="nav-item">
<button className="btn btn-sm btn-outline" onClick={onLogout}>
Logout
</button>
</li>
</ul>
</nav>
);
}Converting a Form
<!-- HTML form with labels, inputs, and validation -->
<form id="contact-form" method="post" enctype="multipart/form-data"
onsubmit="handleSubmit(event)">
<div class="form-group">
<label for="name" class="form-label">Full Name</label>
<input type="text" id="name" name="name" class="form-control"
placeholder="Jane Smith" required maxlength="100"
autocomplete="name">
</div>
<div class="form-group">
<label for="email" class="form-label">Email Address</label>
<input type="email" id="email" name="email" class="form-control"
placeholder="jane@example.com" required autocomplete="email">
</div>
<div class="form-group">
<label for="message" class="form-label">Message</label>
<textarea id="message" name="message" class="form-control" rows="5"
placeholder="Your message..." required></textarea>
</div>
<div class="form-check">
<input type="checkbox" id="consent" name="consent" class="form-check-input" required>
<label for="consent" class="form-check-label">
I agree to the <a href="/privacy">Privacy Policy</a>
</label>
</div>
<button type="submit" class="btn btn-primary" disabled>Send Message</button>
</form>// JSX with controlled components
function ContactForm({ onSubmit }) {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
consent: false,
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const isValid = formData.name && formData.email &&
formData.message && formData.consent;
return (
<form id="contact-form" encType="multipart/form-data" onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="name" className="form-label">Full Name</label>
<input
type="text"
id="name"
name="name"
className="form-control"
placeholder="Jane Smith"
required
maxLength={100}
autoComplete="name"
value={formData.name}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="email" className="form-label">Email Address</label>
<input
type="email"
id="email"
name="email"
className="form-control"
placeholder="jane@example.com"
required
autoComplete="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="message" className="form-label">Message</label>
<textarea
id="message"
name="message"
className="form-control"
rows={5}
placeholder="Your message..."
required
value={formData.message}
onChange={handleChange}
/>
</div>
<div className="form-check">
<input
type="checkbox"
id="consent"
name="consent"
className="form-check-input"
required
checked={formData.consent}
onChange={handleChange}
/>
<label htmlFor="consent" className="form-check-label">
I agree to the <a href="/privacy">Privacy Policy</a>
</label>
</div>
<button type="submit" className="btn btn-primary" disabled={!isValid}>
Send Message
</button>
</form>
);
}Converting an SVG Icon
This is where automatic conversion saves the most time:
<!-- SVG icon from Figma or an icon library (HTML) -->
<svg xmlns="http://www.w3.org/2000/svg"
width="24" height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-check">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>// JSX
function CheckIcon({ size = 24, className }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={`icon icon-check ${className || ''}`}
>
<polyline points="20 6 9 17 4 12" />
</svg>
);
}Converting a Card Component
<!-- Bootstrap-style card in HTML -->
<div class="card" style="width: 18rem; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<img src="..." class="card-img-top" alt="Card image cap">
<div class="card-body">
<h5 class="card-title">Card Title</h5>
<p class="card-text">Some quick example text for this card.</p>
<a href="#" class="btn btn-primary" onclick="event.preventDefault(); handleClick();">
Go somewhere
</a>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">An item</li>
<li class="list-group-item">A second item</li>
</ul>
</div>// JSX component
function Card({ imageSrc, title, text, items, onAction }) {
return (
<div
className="card"
style={{ width: '18rem', boxShadow: '0 4px 6px rgba(0,0,0,0.1)' }}
>
<img src={imageSrc} className="card-img-top" alt="Card image cap" />
<div className="card-body">
<h5 className="card-title">{title}</h5>
<p className="card-text">{text}</p>
<a
href="#"
className="btn btn-primary"
onClick={(e) => { e.preventDefault(); onAction(); }}
>
Go somewhere
</a>
</div>
<ul className="list-group list-group-flush">
{items.map((item, index) => (
<li key={index} className="list-group-item">{item}</li>
))}
</ul>
</div>
);
}JSX Best Practices When Coming From HTML
Prefer className Strings Over Inline Styles
Inline styles in JSX are useful for truly dynamic values that cannot be expressed as class names. For most styling, prefer class names and CSS/Tailwind:
// Prefer: class-based styling
<div className="card shadow-md rounded-lg p-4">
// Use inline styles only for truly dynamic values
<div
className="card"
style={{ backgroundColor: brandColor }} // Dynamic value from props
>Extract Repetitive Structures Into Components
When you find yourself copying the same HTML structure repeatedly, that is a signal to extract it into a component:
// Instead of repeating this pattern:
<li className="nav-item">
<a className="nav-link" href="/dashboard">Dashboard</a>
</li>
// Extract:
function NavItem({ href, children }) {
return (
<li className="nav-item">
<a className="nav-link" href={href}>{children}</a>
</li>
);
}
// Then use it cleanly:
<NavItem href="/dashboard">Dashboard</NavItem>
<NavItem href="/settings">Settings</NavItem>Use Fragments to Avoid Unnecessary Wrapper Divs
Wrapping everything in a <div> to satisfy JSX's single-root requirement adds DOM nodes that interfere with CSS layouts:
// Avoid: extra div breaks flex layout
function TableCells() {
return (
<div> {/* This breaks a table row! */}
<td>Name</td>
<td>Value</td>
</div>
);
}
// Correct: use Fragment
function TableCells() {
return (
<>
<td>Name</td>
<td>Value</td>
</>
);
}Keys in Lists
Whenever you render an array of JSX elements, each must have a unique key prop:
// Wrong: missing key
<ul>
{items.map(item => <li>{item.name}</li>)}
</ul>
// Correct: stable, unique key
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
// Avoid: index as key (causes issues when list can reorder)
{items.map((item, index) => (
<li key={index}>{item.name}</li> // Only acceptable for static, never-reordered lists
))}When to Use an Automatic Converter
Manual conversion is fine for small HTML snippets - a single component or a few lines. But for larger blocks of HTML, automatic conversion is faster and more reliable.
You need an automatic converter when you are:
- Migrating a static HTML site to React - potentially hundreds of templates to convert
- Copying templates from Bootstrap, Tailwind UI, or daisyUI - these ship as HTML; you need JSX
- Converting SVG icons from Figma export - Figma exports SVG with non-JSX attributes
- Porting HTML email templates to React Email or similar
- Copying HTML from documentation or tutorials - most web tutorials show HTML, not JSX
- Converting a landing page or marketing template to Next.js
The HTML to JSX Converter handles all conversions automatically:
classtoclassNamefortohtmlFor- Inline styles to object syntax with camelCase property names
- Self-closing void elements
- All SVG attribute transformations
- camelCase event handlers
- Boolean attribute cleanup
Paste any HTML block, get valid JSX immediately. No setup, no dependencies.
For next steps after conversion:
- Format your JSX with the Code Formatter which uses Prettier to standardize indentation and line length
- Check your Tailwind classes work as expected with the Tailwind to CSS converter if you are using Tailwind utility classes
- Preview your component structure while you work
Open the HTML to JSX Converter - paste HTML, get JSX instantly. Free, no account required, all processing in your browser.
You might also like
Want higher limits, batch processing, and AI tools?