Topic 1.6
Converting HTML to JSX
In one line
To turn HTML into JSX you rename attributes (class, for, camelCase, SVG names like stroke-width), close every tag, swap <!-- --> comments for {/* */}, and know how JSX treats entities, Unicode and whitespace; data-* and aria-* attributes stay as they are.
Think of it like this
Translating a recipe from British to American English. Most words stay the same, but a few must change: "biscuit" becomes "cookie", grams become cups. Translating HTML to JSX is the same: most of it stays, and a short, learnable list of things changes.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Attribute rename
- Writing an HTML attribute with its JSX name, like
tabindex→tabIndex. - data-* attribute
- Your own information stored on an element, like
data-id="7". Stays dashed in JSX. - aria-* attribute
- Information for screen readers, like
aria-label="Close". Stays dashed in JSX. - HTML entity
- A code for a special character, like
©for ©. Works in JSX text, not in braces. - Unicode escape
- A JavaScript way to write any character by number:
"\u20b9"is ₹. - JSX comment
{/* … */}: a JavaScript comment inside braces. HTML comments don't work.- Whitespace
- Spaces, tabs and line breaks. JSX removes line breaks between elements.
- Inline SVG
- An
<svg>drawing written right in the markup, often used for icons.
Step by step
01Start from real HTML
A small newsletter form from an old page. Count the things that must change before you read on.
<!-- Newsletter box -->
<form class="box" onsubmit="return send()">
<label for="mail">Email</label>
<input id="mail" type="email" tabindex="1" autofocus>
<button class="btn" data-track="signup" aria-label="Subscribe">→</button>
</form>02Fix comments and closing tags
<!-- … --> becomes {/* … */} (inside the JSX) or a // comment above it. <input …> becomes <input … />.
03Rename attributes, keep data-* and aria-*
class → className, for → htmlFor, tabindex → tabIndex, autofocus → autoFocus. The string onsubmit handler becomes a function prop (Phase 4 and 5 show how). data-track and aria-label stay exactly as they were. The entity → works as is.
function Signup() {
return (
<form className="box">
{/* Newsletter box */}
<label htmlFor="mail">Email</label>
<input id="mail" type="email" tabIndex={1} autoFocus />
<button className="btn" data-track="signup" aria-label="Subscribe">→</button>
</form>
);
}04Check the whitespace
Where text and an element are on different lines, JSX joins them with no space. Read your converted markup and add {" "} where words would run together.
05Bring in SVG icons
Paste the SVG, camelCase the dashed attributes, drop xmlns, and add aria-hidden="true" if the icon is decoration.
// HTML: <svg viewBox="0 0 24 24"><path d="M4 12l5 5L20 6" stroke-width="2" stroke-linecap="round" fill="none" stroke="currentColor"/></svg>
function CheckIcon() {
return (
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path d="M4 12l5 5L20 6" strokeWidth={2} strokeLinecap="round" fill="none" stroke="currentColor" />
</svg>
);
}Try it yourself
- 1
Remove the forced space
In the receipt, delete
{" "}from line e. Predict the last log line. (e: "Total:₹40", the same as d.) - 2
Change the rating
Render
<Rating stars={5} />. What does a screen reader hear? ("5 out of 5 stars": the label is built from the prop.) - 3
Convert an HTML snippet yourself
Convert
<p class="note">Price drop! <a href="/sale" tabindex="0">See deals→</a></p>into JSX inside the receipt, then compare with the cheat sheet. Onlyclassandtabindexchange.
Code & diagrams
Lines a and b are identical: entities in JSX text, Unicode escapes inside a JavaScript string. Line c joined two text lines with one space. Line d lost the space between `Total:` and the price because a line break between text and a tag is removed; line e puts it back with `{" "}`.
Preview
Console
Nothing logged yet.Expected console output
a: "Tea & biscuits — ₹40 © 2024"
b: "Tea & biscuits — ₹40 © 2024"
c: "Thank you for visiting"
d: "Total:₹40"
e: "Total: ₹40"You write `strokeWidth` and `strokeLinejoin`; React writes `stroke-width` and `stroke-linejoin`. `aria-label` and `data-stars` are written dashed and arrive unchanged. The stars are `aria-hidden`, and the wrapper's label tells a screen reader the rating in words.
Preview
Console
Nothing logged yet.Expected console output
3 out of 5 stars | 3
2 round
0 0 24 24// HTML // JSX
class="x" className="x"
for="email" htmlFor="email"
tabindex="0" tabIndex={0}
maxlength="40" maxLength={40}
readonly readOnly
onclick="save()" onClick={save}
style="color: red" style={{ color: "red" }}
<br> <img src="a.png"> <br /> <img src="a.png" />
<!-- note --> {/* note */}
stroke-width="2" (SVG) strokeWidth={2}
xlink:href="#a" (SVG) xlinkHref="#a" (or plain href="#a")
data-id="7" data-id="7" (unchanged)
aria-label="Close" aria-label="Close" (unchanged)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Keep an HTML comment
In the receipt, replace the {/* … */} line with <!-- old HTML comment -->.
Break #2
Paste SVG attributes unchanged
In StarIcon, change strokeWidth={2} to stroke-width="2".
Break #3
camelCase an aria attribute
In Rating, change aria-label= to ariaLabel=.
Myth vs fact
Myth
HTML pasted into a component just works.
Fact
It usually needs a few changes: attribute names, closing tags, comments, style strings and event handlers. The rules are short and mechanical, and the warnings point at each one.
Myth
Everything in JSX is camelCase.
Fact
data-* and aria-* keep their dashes, and custom properties in style objects ("--x") keep theirs too.
Myth
doesn't work in JSX.
Fact
HTML entities work in JSX text. They don't work inside braces (JavaScript strings), where you use the character or \u00a0.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
JSX's whitespace rule (from the JSX spec): lines of text are trimmed, lines that are empty after trimming are dropped, and the remaining lines are joined with single spaces. That's why indentation never leaks into your text, and why breaks between text and tags vanish.
- ▸
JSX supports all named HTML entities in text and in quoted attribute values (
title="a & b"), but not inside braces. JSX string attributes also don't process JavaScript escapes:title="a\u00a9b"keeps the backslash literally; writetitle={"a\u00a9b"}for the escape. - ▸
For many SVG icons, import them as components with a build plugin (SVGR in Vite:
import Logo from "./logo.svg?react") instead of hand-converting. Icon libraries such as lucide-react ship each icon as a ready React component.
Remember this
- 1
Attribute renames:
class→className,for→htmlFor, and multi-word attributes become camelCase:tabindex→tabIndex,maxlength→maxLength,readonly→readOnly,autocomplete→autoComplete,contenteditable→contentEditable,srcset→srcSet. Event attributes become camelCase props that take a function, not a string:onclick="save()"becomesonClick={save}(events are Phase 4). Thestylestring becomes an object (Topic 1.5). - 2
**
data-*andaria-*keep their dashes.**data-id="7",aria-label="Close"andaria-expanded={open}are written exactly as in HTML. Custom data attributes are for your own information on elements; ARIA attributes describe elements to screen readers and other assistive technology (Phase 18 and the/htmlcourse). WritingariaLabelis a mistake that React warns about. - 3
Comments: HTML's
<!-- … -->is a syntax error in JSX. Inside JSX, write a JavaScript comment wrapped in braces:{/* note */}. Outside JSX (in the function body) normal//comments work. - 4
Entities and Unicode: inside JSX text, HTML entities work:
&,©,—, ,₹(₹). Inside braces you are in JavaScript, where entities are just text, so use the character itself ("©") or a JavaScript escape ("\u00a9"). Since source files are UTF-8, typing the real character (₹,©,—) is usually the clearest choice. - 5
Whitespace rules differ from HTML. Text on one line keeps its spaces. A line break plus indentation between pieces is removed entirely: a line ending in
Total:followed by<strong>on the next line renders asTotal:₹40, with no space. Line breaks *inside* a run of text become one space. To force a space where lines join, write{" "}. - 6
Inline SVG works in JSX, which is handy for icons. Copy the
<svg>, then camelCase its dashed attributes:stroke-width→strokeWidth,stroke-linejoin→strokeLinejoin,fill-rule→fillRule,clip-path→clipPath.viewBoxis already camelCase;xmlnscan be removed. Decorative icons getaria-hidden="true"so screen readers skip them; meaningful ones need a label.
Explain it without notes
List the changes needed to turn an HTML snippet into JSX.
How do comments work in JSX?
Explain JSX's whitespace rules with an example.
Why do data-* and aria-* keep their dashes when everything else is camelCase?
Practice
Convert to JSX: <!-- card --><div class="card" data-id="3" tabindex="0"><img src="p.png" alt=""><p>Price: ₹99</p></div>.
Convert this icon to a component: <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke-width="2" stroke="black" fill="none"/></svg>, hidden from screen readers.
Trade-offs
- ↔
Hand-converting HTML is fine for a snippet; for big templates or many SVG files, an automated converter (an HTML-to-JSX tool or SVGR) is faster and avoids typos, but still review the output for accessibility and whitespace.
Done when you can
I can convert an HTML snippet to JSX without warnings.
I write comments in JSX as
{/* */}.I know where entities work and where to use Unicode characters.
I can predict and fix whitespace between text and tags.
I can paste an SVG icon and camelCase its attributes.
I keep
data-*andaria-*dashed.