Stop manually fixing attribute names. Master the art of converting legacy HTML and complex SVGs into production-ready React code with clinical precision.
Category: Dev Tools
Last updated: 2026-01-01
Estimated reading time: 18–25 minutes
A practical, zero-fluff playbook for converting any HTML or SVG into robust, accessible React components — without runtime bugs, hydration issues, or accessibility 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
JSX is a JavaScript syntax extension that compiles to function calls (for example, React's jsx/jsxs). Because it is code, not a template language:
class → className and for → htmlFor (on label).{expression}.{/* ... */}.Helpful references:
Use this quick pass before you dig into details:
class → classNamefor (on label) → htmlForstyle='...' → style={{ ... }} with camelCase keysimg, br, input, source, meta, link, track → <... />{condition} for dynamicstabindex → 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 → allowFullScreendata-* and aria-* stay kebab-case (unchanged)stroke-width → strokeWidth, fill-rule → fillRule, clip-path → clipPath, stroke-linecap → strokeLinecap, stroke-linejoin → strokeLinejoin, stop-color → stopColor, xlink:href → xlinkHref<> ... </><!-- ... --> → {/* ... */}<!-- 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>
<!-- 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 for autocomplete and safety.
<!-- Before (HTML) -->
<img src='/logo.png' alt='Logo'>
<br>
// After (JSX)
<img src='/logo.png' alt='Logo' />
<br />
<!-- 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>
<!-- Before (HTML) -->
<button onclick='doThing()'>Run</button>
// After (JSX)
<button onClick={doThing}>Run</button>
<div data-user-id='42' aria-live='polite' />
// Good
<ul>
{items.map(item => (
<li key={item.id}>{item.label}</li>
))}
</ul>
Use stable, unique keys (not array index for re-orderable lists).
<!-- Before (HTML) -->
<!-- todo: replace icon -->
// After (JSX)
{/* todo: replace icon */}
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 → classNamefor (only on label) → htmlFortabindex → tabIndexreadonly → readOnlymaxlength → maxLengthminlength → minLengthcontenteditable → contentEditablespellcheck → spellChecksrcset → srcSetcrossorigin → crossOriginenctype → encTypeautocomplete → autoCompleteaccept-charset → acceptCharsethttp-equiv → httpEquivframeborder → frameBorderallowfullscreen → allowFullScreen (iframe)referrerpolicy → referrerPolicynovalidate → noValidate (form)autoplay → autoPlay (media)playsinline → playsInline (video)srcdoc → srcDoc (iframe)inputmode → inputModeautocapitalize → autoCapitalize (check browser support)aria-* → unchanged (kebab-case)SVG mapping highlights:
fill-rule → fillRulestroke-width → strokeWidthstroke-linecap → strokeLinecapstroke-linejoin → strokeLinejoinclip-path → clipPathstop-color → stopColorxlink:href → xlinkHrefviewBox → viewBox (already camelCase)preserveAspectRatio → preserveAspectRatio (already camelCase)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.
In HTML, style is a serialized string. In JSX it’s a plain JavaScript object:
background-color → backgroundColor.'1.5rem', '2ch', '50%'.-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)',
}
When to use inline styles:
When not to use inline styles:
Raw SVG pasted into JSX often breaks due to attribute casing and XML-specific features. Typical fixes:
strokeWidth={2} instead of '2'.xlink:href becomes xlinkHref (note: SVG2 deprecates xlink; many assets still use it).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 should:
Recommended:
JSX makes semantic, accessible markup easier, but a naive copy-paste from HTML can regress a11y. Prioritize:
aria-expanded, aria-selected).htmlFor (on label) and matching input id.alt. Decorative images can use alt=''.tabIndex. Only add positive tab indexes for special cases (generally avoid).aria-live='polite' for non-critical updates; assertive sparingly.Audit tools:
className. class compiles but can lead to warnings or unexpected behavior with TypeScript/linters.htmlFor on label. It must match the id of the related input.{condition} for dynamics: <button disabled={isSaving} />.<input />, <img />, <br />, <hr />, <meta />, <link />, <source />, <track />.<select>value (or defaultValue) on the select. Example:<select value={role} onChange={e => setRole(e.target.value)}>
<option value='user'>User</option>
<option value='admin'>Admin</option>
</select>
<select defaultValue='user'>.{/* comment */} in JSX. in JSX just like HTML. For literal braces, use {'{'} ... {'}'}.onclick → onClick, onchange → onChange, etc. Use functions, not strings: <button onClick={handleClick} />.dangerouslySetInnerHTML={{ __html: safeHtml }} with a trusted sanitizer.<script> tags in JSX won’t execute as you expect when injected dynamically. Move logic into React effects.Sometimes you must render CMS or legacy HTML. Keep the bar high on safety:
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
href, src) without validation.rel='noopener noreferrer' with target='_blank'.Date.now() or Math.random() directly in render output; compute them in useEffect or provide deterministic values.Strong static checks catch most conversion mistakes early.
// tsconfig snippet (JSX transform)
{
"compilerOptions": {
"jsx": "react-jsx", // modern JSX transform
"strict": true,
"noUncheckedIndexedAccess": true
}
}
ESLint essentials:
eslint-plugin-react for React/JSX rules.eslint-plugin-react-hooks for hooks rules.eslint-plugin-jsx-a11y for accessibility.react/jsx-uses-react only if you are on the classic transform; otherwise rely on the automatic runtime.Prettier:
Type definitions:
@types/react and @types/react-dom for TypeScript autocomplete of props and CSSProperties.Example test:
import { render, screen } from '@testing-library/react'
import { Card } from './Card'
test('renders title and description', () => {
render(<Card title='Hello' description='World' />)
expect(screen.getByRole('heading', { name: 'Hello' })).toBeInTheDocument()
expect(screen.getByText('World')).toBeInTheDocument()
})
When the HTML/SVG isn’t tiny, automation pays for itself.
ZenixTools (HTML/SVG → JSX converter):
SVGO:
SVGR:
title, className, and role.Editor integrations:
class insertion.Marketing pages or CMS exports:
Old jQuery templates:
onclick/onchange strings with real event handlers.Email templates in React (MJML/React-email):
style or options. Memoize when necessary.<!-- Before -->
<section class='hero' style='background-image: url(/hero.jpg); height: 420px'>
<h1>Welcome</h1>
<p>Build faster with React</p>
</section>
// After
<section
className='hero'
style={{ backgroundImage: 'url(/hero.jpg)', height: 420 }}
>
<h1>Welcome</h1>
<p>Build faster with React</p>
</section>
<!-- Before -->
<form novalidate>
<label for='email'>Email</label>
<input id='email' type='email' required>
<button disabled>Submit</button>
</form>
// After (uncontrolled example)
<form noValidate onSubmit={handleSubmit}>
<label htmlFor='email'>Email</label>
<input id='email' type='email' required aria-invalid={!isValid} />
<button disabled={isSubmitting}>Submit</button>
</form>
<!-- Before -->
<svg width='24' height='24' stroke-width='1.5' stroke-linejoin='round'>
<path d='M2 12h20' />
</svg>
// After
<svg width={24} height={24} strokeWidth={1.5} strokeLinejoin='round'>
<path d='M2 12h20' />
</svg>
Can I keep class in JSX?
className. Some tooling may tolerate class, but it’s not the recommended API and may trip TypeScript or linters.Do I need curly braces for strings?
<div title='Hello' /> is fine. Use {} for expressions: <div title={greeting} />.How do I conditionally add attributes?
<input disabled={isSaving || undefined} /> or {condition && <span>....How do I add multiple classes dynamically?
className={[base, isActive && 'active'].filter(Boolean).join(' ')}.Why doesn’t my script tag inside JSX run?
dangerouslySetInnerHTML won’t execute like parser-inserted scripts. Use effects or dynamic module loading.How do I preselect an option?
value/defaultValue on select, not selected on option.Can I keep data-* and aria-* lowercase?
What about ref attributes?
Use this final pass before merging:
className, htmlFor, camelCased props everywhere except data-*/aria-*.onClick, onChange, etc., use functions (no strings).rel='noopener noreferrer' for external links.I’m a senior front-end engineer and technical writer who has led React migrations for design systems, marketing sites, and high-traffic SaaS apps. I specialize in safe HTML/SVG → JSX conversions, accessibility-first audits, and setting up linting and testing workflows that prevent regressions at scale.
Appendix: Quick Attribute Reference (copy-paste)
HTML → JSX quick map
- class → className
- for → htmlFor (label only)
- 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)
- inputmode → inputMode
- autocapitalize → autoCapitalize
- aria-* → unchanged (kebab-case)
- data-* → unchanged (kebab-case)
SVG
- fill-rule → fillRule
- stroke-width → strokeWidth
- stroke-linecap → strokeLinecap
- stroke-linejoin → strokeLinejoin
- clip-path → clipPath
- stop-color → stopColor
- xlink:href → xlinkHref
A complete, human-friendly guide to convert to WebP for faster sites and better SEO. Learn benefits, step-by-step workflows, code examples, and expert tips. Use ZenixTools to convert to WebP in seconds.
A practical, expert guide to convert base64 to string across languages, with steps, examples, pitfalls, and best practices.
data-* → unchanged (kebab-case)ref is a special React prop for DOM nodes or components. Use useRef and pass ref={node => ...} or ref={refObj}.