HTML to JSX: The 2026 Guide to Clean React Components
Category: Dev Tools
Last updated: 2026-01-01
Estimated reading time: 15–20 minutes
At a Glance (Key Takeaways)
- JSX looks like HTML, but it’s JavaScript syntax. Follow JS rules, not HTML quirks.
- Attribute renames: class → className, for → htmlFor, tabindex → tabIndex, readonly → readOnly, srcset → srcSet, and more.
- Inline styles are JS objects, not strings: use camelCase keys and numbers where possible.
- All elements must be explicitly closed: <img />, <br />, <input />.
- SVG attributes must be camelCased: stroke-width → strokeWidth, fill-rule → fillRule, xlink:href → xlinkHref.
- Keep data-* and aria-* exactly as-is (kebab-case) in JSX.
- Use automated tools for large HTML/SVG blocks to avoid subtle runtime bugs.
- Lint, type-check, and snapshot-test your converted components to catch regressions.
Pro tip: For big or messy HTML/SVG, automate the conversion and sanitization to save hours and reduce risk. Try ZenixTools for safe HTML/SVG → JSX conversion: https://www.zenixtools.com
Why JSX Is Not HTML (and Why It Matters)
JSX is a syntax extension for JavaScript that compiles to function calls (e.g., React’s jsx, jsxs). Because JSX is JavaScript, not a template language:
- Attributes sometimes change names to avoid JS keyword conflicts or to match DOM property casing.
- Dynamic values live inside curly braces: {expression}.
- Everything must be valid JavaScript: no dangling tags, no unclosed quotes, no implicit boolean attributes.
Helpful references:
The 60‑Second Conversion Checklist
Use this quick pass before you dig into details:
- class → className
- for (on <label>) → htmlFor
- Inline styles: style="..." → style={{ ... }} with camelCase keys
- Close all tags: <img />, <br />, <input />, <source />, <meta />, <link />
- Boolean attributes: checked, disabled, required → keep as props (checked, disabled). Use {condition} for dynamics
- Common attribute renames: tabindex → tabIndex, readonly → readOnly, maxlength → maxLength, minlength → minLength, contenteditable → contentEditable, spellcheck → spellCheck, srcset → srcSet, crossorigin → crossOrigin, enctype → encType, autocomplete → autoComplete, accept-charset → acceptCharset, http-equiv → httpEquiv, frameborder → frameBorder
- data-* and aria-* stay kebab-case
- SVG: stroke-width → strokeWidth, fill-rule → fillRule, clip-path → clipPath, stroke-linecap → strokeLinecap, stroke-linejoin → strokeLinejoin, stop-color → stopColor, xlink:href → xlinkHref
- Multiple roots? Wrap with a fragment: <>...</>
- Comments: <!-- ... --> → {/* ... */}
Before/After Essentials (HTML → JSX)
Classes and Labels
<!-- Before (HTML) -->
<div class="card primary">Hello</div>
<label for="email">Email</label>
// After (JSX)
<div className="card primary">Hello</div>
<label htmlFor="email">Email</label>
Styles: String → Object
<!-- Before (HTML) -->
<div style="background-color: red; margin-top: 8px; opacity: 0.9"></div>
// After (JSX)
<div
style={{
backgroundColor: 'red',
marginTop: 8,
opacity: 0.9,
}}
/>
Tip: In TypeScript, annotate as React.CSSProperties.
Self-Closing Enforcement
<!-- Before (HTML) -->
<img src="/logo.png" alt="Logo">
<br>
// After (JSX)
<img src="/logo.png" alt="Logo" />
<br />
Boolean Attributes
<!-- Before (HTML) -->
<input type="checkbox" checked>
<button disabled>Save</button>
// After (JSX)
<input type="checkbox" checked />
<button disabled>Save</button>
// Dynamic
<input type="checkbox" checked={isSelected} />
<button disabled={isSaving}>Save</button>
Events and Expressions
<!-- Before (HTML) -->
<button onclick="doThing()">Run</button>
// After (JSX)
<button onClick={doThing}>Run</button>
Data and ARIA Attributes
- Keep as-is (kebab-case) in JSX.
- Do not camelCase these.
<div data-user-id="42" aria-live="polite" />
The Complete Attribute Map (Cheat Sheet)
Use this as a handy reference for the most common renames. If an attribute isn’t listed and React doesn’t recognize it, check the React DOM attributes link above.
HTML → JSX mapping highlights:
- class → className
- for → htmlFor (only on label)
- tabindex → tabIndex
- readonly → readOnly
- maxlength → maxLength
- minlength → minLength
- contenteditable → contentEditable
- spellcheck → spellCheck
- srcset → srcSet
- crossorigin → crossOrigin
- enctype → encType
- autocomplete → autoComplete
- accept-charset → acceptCharset
- http-equiv → httpEquiv
- frameborder → frameBorder
- allowfullscreen → allowFullScreen (iframe)
- referrerpolicy → referrerPolicy
- novalidate → noValidate (form)
- autoplay → autoPlay (media)
- playsinline → playsInline (video)
- srcdoc → srcDoc (iframe)
- srcset (img, source) → srcSet
- inputmode → inputMode
- capture → capture (unchanged, but value is string/boolean)
- autocapitalize → autoCapitalize (not universal; check browser support)
- aria-* → unchanged (kebab-case)
- data-* → unchanged (kebab-case)
SVG mapping highlights:
- fill-rule → fillRule
- stroke-width → strokeWidth
- stroke-linecap → strokeLinecap
- stroke-linejoin → strokeLinejoin
- clip-path → clipPath
- stop-color → stopColor
- viewBox → viewBox (already camelCase)
- preserveAspectRatio → preserveAspectRatio (already camelCase)
- xlink:href → xlinkHref (SVG2 deprecates xlink, but React supports xlinkHref)
Note: Unknown attributes generally pass through to the DOM in modern React, but TypeScript may flag them. Prefer the React-recognized camelCase form for best DX and typings.
CSS in JS: The Style Object You’ll Actually Use
In HTML, style is a serialized string. In JSX it’s a plain JavaScript object:
- Keys are camelCased: background-color → backgroundColor
- Units: numbers default to px for many properties; otherwise use strings: '1.5rem', '2ch', '50%'
- Vendor prefixes: capitalize the prefix: -webkit-line-clamp → WebkitLineClamp
Examples:
<div style={{ width: 320, lineHeight: '1.5', WebkitLineClamp: 3 }} />
TypeScript:
const box: React.CSSProperties = {
backgroundColor: '#0ea5e9',
marginTop: 12,
transform: 'translateY(2px)',
};
Avoid style objects for large styling needs; prefer CSS Modules, Tailwind, or styled solutions. Use inline styles for dynamic one-offs or when computing values at runtime.
SVGs: The Tricky Part (Use a Converter for Anything Big)
Raw SVG pasted into JSX often breaks due to attribute casing. Typical fixes:
- fill-rule → fillRule
- stroke-width → strokeWidth
- clip-path → clipPath
- stroke-linecap → strokeLinecap
- stroke-linejoin → strokeLinejoin
- stop-color → stopColor
- xlink:href → xlinkHref
- viewBox and preserveAspectRatio remain as-is
Example:
<!-- Before (SVG in HTML) -->
<svg viewBox="0 0 24 24" fill-rule="evenodd">
<path stroke-width="2" clip-path="url(#clip)" xlink:href="#shape" />
</svg>
// After (JSX)
<svg viewBox="0 0 24 24" fillRule="evenodd">
<path strokeWidth={2} clipPath="url(#clip)" xlinkHref="#shape" />
</svg>
For long, designer-exported SVGs (hundreds or thousands of lines), manual conversion is error-prone. A professional sanitizer/converter:
- Renames attributes to React’s camelCase
- Removes unsafe inline scripts
- Preserves IDs/defs while avoiding collisions
- Minimizes file size without breaking rendering
Recommended:
Accessibility First: JSX Patterns That Keep Assistive Tech Happy
- Labels: Associate inputs with labels via htmlFor and id
- ARIA: aria-* stays kebab-case; only add ARIA when semantics aren’t enough
- Roles: Prefer semantic HTML elements first (<button>, <nav>, <header>, <main>)
- Keyboard: Use tabIndex for custom focus order; avoid removing focus outlines without proper alternatives
- Live regions: aria-live="polite" or "assertive" for announcements
Example:
<label htmlFor="email">Email</label>
<input id="email" type="email" aria-describedby="email-help" />
<small id="email-help">We’ll never share your email.</small>
- Controlled inputs: value and onChange fully managed by React state
- Uncontrolled inputs: defaultValue/defaultChecked set initial value; read via refs when needed
Examples:
// Controlled
function NameField() {
const [name, setName] = React.useState('')
return (
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
autoComplete="name"
/>
)
}
// Uncontrolled
function EmailField() {
return <input type="email" defaultValue="user@example.com" />
}
Common attribute mappings for forms:
- autocomplete → autoComplete
- readonly → readOnly
- minlength/maxlength → minLength/maxLength
- novalidate → noValidate (on <form>)
Events in JSX: It’s JavaScript, Not Strings
- Use camelCase handlers (onClick, onChange, onSubmit, onKeyDown)
- Pass a function, not a string
- Prevent default in handler functions, not inline strings
<button onClick={() => doThing()}>Run</button>
<form onSubmit={(e) => { e.preventDefault(); submit(); }}>
...
</form>
- Comments: {/* comment */} (HTML-style comments won’t compile)
- Entities: works, but {'\u00A0'} is explicit and unambiguous in code
- Whitespace: JSX collapses spaces similarly to HTML; for non-breaking spaces use {'\u00A0'}
Multiple Roots and Fragments
In JSX, a component must return a single parent node. Wrap siblings with a fragment to avoid extra DOM nodes:
return (
<>
<Header />
<Main />
</>
)
You can also use the long form: <React.Fragment key={...}> when you need a key.
Security: Sanitizing Dangerous HTML
Only use dangerouslySetInnerHTML with trusted/sanitized content. Never pass raw user input.
import DOMPurify from 'dompurify'
function SafeHtml({ html }) {
return (
<div
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}
/>
)
}
If you must inject large third-party HTML blocks, run them through a sanitizer and JSX converter first. Tools like ZenixTools can remove unsafe attributes/scripts during conversion.
Real-World Conversion Workflow (Step-by-Step)
Follow this repeatable process for reliable outcomes:
- Normalize the HTML
- Ensure valid nesting and closing tags
- Remove inline event handlers (onclick, onmouseover) to replace with JSX handlers
- Consolidate duplicate IDs and fix invalid attributes
- Run an Automated Converter
- Batch-convert HTML/SVG to JSX
- Apply attribute casing rules and boolean normalization
- Strip script/style injection vectors
- Address Edge Cases
- Fix label/for → htmlFor
- Convert styles to objects, or move repetitive styles to CSS classes
- Wrap multiple roots with fragments
- Refactor into Components
- Extract repeated blocks into functional components with props
- Add keys to mapped lists
- Add prop types (TypeScript interfaces) for safer reuse
- Lint and Type-Check
- ESLint + eslint-plugin-react + react/no-unknown-property
- Prettier for consistent formatting
- TypeScript with React types (e.g., React.CSSProperties, React.SVGProps)
- Test and Ship
- Snapshot-test converted markup
- Unit-test event handlers
- Visual regression tests for complex UIs
Example: Converting a Marketing Card (HTML → JSX Component)
Original HTML snippet:
<div class="card primary">
<img src="/img/hero.png" alt="Hero" class="card__image">
<h3 class="card__title">Welcome</h3>
<p class="card__copy">Fast, safe JSX conversion.</p>
<button class="btn" onclick="signup()" disabled>Get Started</button>
</div>
Refactored JSX/TSX component:
import * as React from 'react'
type CardProps = {
title: string
copy: string
imageSrc: string
onSignup?: () => void
disabled?: boolean
}
export function MarketingCard({ title, copy, imageSrc, onSignup, disabled }: CardProps) {
return (
<div className="card primary">
<img src={imageSrc} alt={title} className="card__image" />
<h3 className="card__title">{title}</h3>
<p className="card__copy">{copy}</p>
<button className="btn" onClick={onSignup} disabled={disabled}>Get Started</button>
</div>
)
}
Regex Power-Ups (Speed Up the Last 20%)
Use with caution and always review diffs.
-
class → className (skip already-correct cases)
- Find:
\bclass="
- Replace:
className="
-
label for → htmlFor (limit to <label> tags)
- Find:
<label([^>]*?)\sfor="([^">]+)"
- Replace:
<label$1 htmlFor="$2"
-
Self-close common void elements
- Find:
<(img|br|hr|input|meta|link)([^>/]*?)>
- Replace:
<$1$2 />
-
Inline style detection (convert manually or via script)
- Find:
style="([^"]+)"
- Extract and convert rules to a JS object
Quick Node utility to convert a CSS style string to a JS object:
function styleStringToObject(s) {
return s.split(';')
.map(x => x.trim())
.filter(Boolean)
.reduce((acc, decl) => {
const [prop, val] = decl.split(':')
if (!prop || !val) return acc
const camel = prop.trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase())
const v = val.trim()
acc[camel] = /^\d+(\.\d+)?$/.test(v) ? Number(v) : v
return acc
}, {})
}
Server, Client, and the JSX Runtime in 2026
- JSX on the server (SSR/RSC): JSX compiles on the server too. Avoid browser-only APIs in server components
- Event handlers only run on the client: wire client components where interactivity is needed
- Hydration mismatches: ensure server and client render the same initial markup (no random IDs without stable seeds)
Framework notes:
- Next.js/Remix: Server-first by default; collocate interactive pieces in client components
- Vite + React: Use the automatic JSX runtime; no need to import React for jsx in modern setups
- Extract icons/SVGs into reusable components and de-duplicate IDs
- Prefer CSS classes over massive style objects for static styling
- Lazy-load heavy components; prefetch above-the-fold assets
- Remove inline event handler strings; use memoized callbacks if necessary (React.useCallback) for hot paths
Linting, Types, and Testing
- ESLint: eslint-plugin-react and rules like react/no-unknown-property, react/jsx-no-duplicate-props, react/jsx-key
- TypeScript: use React types like React.CSSProperties, React.HTMLAttributes<T>, React.SVGProps<T>
- Testing: snapshot-test converted components and run visual regression tests for complex UIs
Helpful docs:
Automation: Don’t Convert by Hand Every Time
Manual conversion works for a few nodes; not for full pages or SVG sprites.
Automate to:
- Batch-convert HTML → JSX with correct attribute casing
- Normalize boolean and numeric props
- Sanitize SVG and strip unsafe content
Tools to try:
- ZenixTools: automated HTML/SVG → JSX conversion and sanitization: https://www.zenixtools.com
- SVGR: turn SVGs into React components
- Prettier + ESLint: clean, consistent output after conversion
Common Errors and How to Fix Them Fast
- Warning: Unknown DOM property 'class'. Did you mean 'className'? → Rename class → className
- Warning: Invalid DOM property 'for'. Did you mean 'htmlFor'? → Rename for → htmlFor on label
- TypeError: Cannot read properties of undefined (reading 'value') → Controlled input missing onChange or value mismatch
- Hydration mismatch → Ensure deterministic IDs and identical server/client render
- SVG not rendering or attributes ignored → CamelCase SVG attributes; remove inline XML declarations or DOCTYPEs
Troubleshooting Checklist Before You Commit
SEO & Sharing Checklist for JSX Pages
- Title: Target keyphrase, under ~60 chars
- Meta description: Compelling summary, under ~160 chars
- Canonical: Point to preferred URL
- Open Graph/Twitter: og:title, og:description, og:image; twitter:card
- Internal links: Connect related React/JSX content and tools (e.g., ZenixTools)
- Structured data: Article/BlogPosting JSON-LD
Copy-Paste Structured Data (JSON-LD)
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "HTML to JSX: The 2026 Guide to Clean React Components",
"description": "A practical guide to converting HTML and SVG to JSX with examples, checklists, and tools.",
"author": {
"@type": "Person",
"name": "Senior SEO Content Strategist"
},
"datePublished": "2026-01-01",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://www.your-domain.com/html-to-jsx-guide"
}
}
FAQ: Quick Answers for Teams
Q: How do I convert a big designer-exported SVG to JSX safely?
A: Use a converter that camelCases attributes and sanitizes content. SVGR helps turn SVGs into components. ZenixTools can batch-convert and sanitize at once, preventing subtle runtime errors.
Q: Should I camelCase data-* or aria-* attributes?
A: No. Keep them exactly as-is (kebab-case) in JSX.
Q: Do I need to import React at the top of every file?
A: With the modern automatic JSX runtime (Babel/TypeScript config), you typically don’t. Your framework/tooling may vary.
Q: Why does my onClick not fire?
A: Ensure you passed a function (onClick={handler}) and that the element isn’t covered by another element with pointer-events or disabled due to state.
Q: My input doesn’t update when I type. Why?
A: Controlled inputs require value and onChange in sync. If you provide value without onChange, it becomes read-only.
Q: What about dangerouslySetInnerHTML?
A: Only use with sanitized or trusted HTML. Prefer pure JSX whenever possible for maintainability and security.
Q: Can I keep raw HTML as a string and render it?
A: Yes, via dangerouslySetInnerHTML, but sanitize first and accept the trade-offs: harder testing, riskier changes, and no type safety.
Reference: Extended HTML → JSX Attribute Map
Below are additional mappings you’ll encounter often. When in doubt, check React’s DOM attributes list.
- accept-charset → acceptCharset
- autocapitalize → autoCapitalize
- autocomplete → autoComplete
- autofocus → autoFocus
- autoplay → autoPlay
- cellpadding → cellPadding
- cellspacing → cellSpacing
- charset → charSet
- class → className
- colspan → colSpan
- contenteditable → contentEditable
- contextmenu → contextMenu
- crossorigin → crossOrigin
- datetime → dateTime
- enctype → encType
- for → htmlFor (label)
- formnovalidate → formNoValidate
- frameborder → frameBorder
- hreflang → hrefLang
- http-equiv → httpEquiv
- inputmode → inputMode
- maxlength → maxLength
- minlength → minLength
- novalidate → noValidate
- readonly → readOnly
- referrerpolicy → referrerPolicy
- rowspan → rowSpan
- spellcheck → spellCheck
- srcdoc → srcDoc
- srcset → srcSet
- tabindex → tabIndex
- usemap → useMap
SVG highlights (repeat):
- alignment-baseline → alignmentBaseline
- clip-path → clipPath
- color-interpolation-filters → colorInterpolationFilters
- dominant-baseline → dominantBaseline
- fill-opacity → fillOpacity
- fill-rule → fillRule
- marker-end → markerEnd
- marker-mid → markerMid
- marker-start → markerStart
- stop-color → stopColor
- stop-opacity → stopOpacity
- stroke-dasharray → strokeDasharray
- stroke-dashoffset → strokeDashoffset
- stroke-linecap → strokeLinecap
- stroke-linejoin → strokeLinejoin
- stroke-miterlimit → strokeMiterlimit
- stroke-opacity → strokeOpacity
- stroke-width → strokeWidth
- text-anchor → textAnchor
- vector-effect → vectorEffect
- xlink:href → xlinkHref
Putting It All Together
Convert small snippets by hand using the checklist; for everything else, automate. Keep accessibility top-of-mind, camelCase attributes correctly, and prefer semantic HTML elements. Use TypeScript for stronger safety, ESLint to catch unknown properties, and snapshot tests to prevent regressions. For SVGs, rely on a professional converter/sanitizer to avoid subtle breakage.
Need a fast, safe conversion? Start here: https://www.zenixtools.com