Writing markup with JSX
See what JSX compiles to, jsx() calls from react/jsx-runtime, and why that gives JSX its rules: one root, closed tags, className, htmlFor, and camelCase.
- 01Read what JSX compiles toTurn a tag into its
jsx()call fromreact/jsx-runtime, say what the element object it returns holds, and compare it with the olderReact.createElement. - 02Explain JSX's rulesSay why a component returns one root (or a Fragment), why every tag closes, and why attributes are spelled
className,htmlFor, and in camelCase. - 03Turn HTML into JSXConvert real HTML into a working component, and read the compiler's errors when something is left the HTML way.
HTML that doesn't paste
In Your first component, every component returned markup that looked like HTML. That markup is JSX. It looks so much like HTML that the first thing people try is pasting HTML into a component. Here is a sign-up box for a tea shop, as plain HTML. Its result is live:
<h2>Join the tea club</h2><p class="hint">One email a month.<br>New teas first.</p><label for="email">Email</label><input id="email" type="email"><button>Join</button>Five lines: a heading, a hint with a line break, a label, an email box, and a button. Now the same five lines, pasted into a component as they are:
export default function JoinBox() { return ( <h2>Join the tea club</h2> <p class="hint">One email a month.<br>New teas first.</p> <label for="email">Email</label> <input id="email" type="email"> <button>Join</button> );}Nothing shows. The result is a syntax error on line 4: Unexpected token, expected ",". The message is puzzling, because the compiler is confused: line 3 is already a whole tag, and in JavaScript nothing can follow it inside the brackets of a return. That's one of four things to fix. Here is the box as JSX:
export default function JoinBox() { return ( <section className="join-box"> <h2>Join the tea club</h2> <p className="hint">One email a month.<br />New teas first.</p> <label htmlFor="email">Email</label> <input id="email" type="email" /> <button>Join</button> </section> );}- One root. A
<section>on line 3 holds everything, and closes on line 9. - Closed tags.
<br>became<br />on line 5, and the<input>on line 7 ends with/>. classNamein place ofclass, on lines 3 and 5.htmlForin place offor, on line 6. Click the word Email in the result: the cursor jumps into the box, becausehtmlForlinks the label to the input with the idemail.
These rules aren't a list to memorize. They all come from one fact: before your code runs, every JSX tag becomes a JavaScript function call.
jsx() call before the code runs, and the call returns an element: a plain object that describes the tag. React reads elements like this one to build the page.At a busy café you say your order however you like, and the cashier writes it on a slip with fixed boxes. The kitchen never hears you. It works only from slips, so each order must fit the slip's boxes: one drink per slip, every box filled in the slip's own words.
- In real life: You say "a green tea, large, no sugar"
- In React: You write
<h3 className="title">Green tea</h3> - In real life: The cashier writes it on a slip with fixed boxes: drink, size, notes
- In React: The compiler writes
jsx("h3", { className: "title", children: "Green tea" }) - In real life: The slip itself, a piece of paper
- In React: The element: a plain object with
typeandprops - In real life: The kitchen makes the drink from the slip
- In React: React makes the real
<h3>from the element
JSX is a way to write markup inside JavaScript. Before the code runs, a compiler turns every tag into a call to a function, jsx(), that returns a plain object describing the tag. Because every tag is a JavaScript call, JSX follows JavaScript's rules, not HTML's.
Every tag becomes a function call
Browsers don't understand JSX. A compiler, a tool that rewrites code, turns it into plain JavaScript first: Babel, TypeScript, esbuild, or Sucrase, usually run for you by your build tool. Every live example on this site is compiled by Sucrase, in your browser, a moment before it runs. Here is a small card:
export default function TeaCard() { return ( <article className="tea-card"> <h3>Green tea</h3> <p>$3</p> </article> );}And here is what a compiler really turns it into. This is the output of TypeScript's compiler with the setting jsx: "react-jsx", React's modern JSX transform, formatted with Prettier. Babel and Sucrase write the same calls; the lesson's tests compile the card with all three and compare.
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export default function TeaCard() {
return _jsxs("article", {
className: "tea-card",
children: [
_jsx("h3", { children: "Green tea" }),
_jsx("p", { children: "$3" }),
],
});
}Read it line by line:
- Line 1 imports two functions,
jsxandjsxs, fromreact/jsx-runtime, a part of thereactpackage made for compilers. The compiler added this line; you never write it. - Line 3: the
<article>tag became a call. Its first argument is the type, the tag's name as a string,"article". - Lines 3 to 9: its second argument is one object, the props. The attribute became a key on line 4,
className: "tea-card", and the tags inside becamechildren(lines 5 to 8). - Lines 6 and 7: each child is its own call, with its text as
children. A tag with one child usesjsx; the article, with two children in an array, usesjsxs. They build the same thing.
A capitalized tag works the same way, with one difference you met in the last lesson: <TeaCard /> becomes jsx(TeaCard, {}), with your function itself as the type, not a string.
So what does a jsx() call give back? An element: a plain JavaScript object that describes the tag, as you saw in How React works. Now you know where it comes from. This example stores one in a constant and prints two of its fields. Press Edit & run if the console isn't showing:
const title = <h3 className="title">Green tea</h3>; console.log(title.type);console.log(title.props); export default function App() { return title;}Line 1 makes the element; nothing is on the page yet. Line 3 prints its type, h3, and line 4 its props: an object with className: "title" and children: "Green tea". Line 7 returns that same object, and React turns it into the real <h3> you see. An element is only a description; React reads it to decide what to put on the page. (It has a few more fields, like key and a hidden tag that marks it as a React element. React elements and the JSX runtime, in the course's last stage, looks at every one.)
The functions the compiler imports are in packages/react/src/jsx/ReactJSX.js, at release v19.3.1, the version these examples run. It exports jsx, jsxs, and Fragment. In production both jsx and jsxs are jsxProd, at line 291 of ReactJSXElement.js, which takes the props object as it is and hands it to ReactElement, at line 170, the function that builds the object with type, key, and props.
Older React code compiled JSX differently. The classic transform turned every tag into React.createElement(type, props, ...children). Here is the same card compiled the classic way (TypeScript with jsx: "react"), from a file that started with import React from "react":
import React from "react";
export default function TeaCard() {
return React.createElement(
"article",
{ className: "tea-card" },
React.createElement("h3", null, "Green tea"),
React.createElement("p", null, "$3"),
);
}The calls on lines 3, 6, and 7 name React, so every file with JSX had to import React (line 1), even when it used nothing else from it. The children are extra arguments (lines 6 and 7), and a tag without attributes gets null. React 17, in 2020, introduced the modern transform, which imports jsx itself. The React 19 upgrade guide made it required: React 19 uses it for features like passing ref as a prop, and in development it warns when an app still uses the classic one.
How to read this table: each row asks the same question of both transforms.
| Question | Classic: React.createElement | Modern: jsx() |
|---|---|---|
| What does a tag turn into? | A call to React.createElement(type, props, ...children). | A call to jsx(type, props) or jsxs(type, props), with the children inside props. |
| Who imports the function? | You: every file with JSX needs import React from "react". | The compiler: it adds import { jsx } from "react/jsx-runtime" itself. |
| Where do the children go? | Extra arguments after the props. | props.children: one child, or an array for several. |
| Does React 19 accept it? | It warns that your app uses an outdated JSX transform. | Yes. React 19 needs it, for features like ref as a prop. |
| Can you call it yourself? | Yes. createElement is still part of React, for building elements without JSX. | You could, but react/jsx-runtime is made for compilers. By hand, use createElement. |
createElement itself isn't gone. It's still the way to build elements by hand, without JSX, and it builds the same elements. This card has no JSX at all, and looks exactly like the one above:
import { createElement } from "react"; export default function TeaCard() { return createElement( "article", { className: "tea-card" }, createElement("h3", null, "Green tea"), createElement("p", null, "$3"), );}Line 1 imports createElement by name. Lines 4 to 9 are three calls: the article, with its props on line 6, and its two children on lines 7 and 8. JSX is a shorter way to write calls like these; you can always do without it, and nobody does, because the markup is much easier to read.
Now compile your own. Pick an example or type any JSX in the box, and switch between the two transforms. The two last examples show the compiler stopping: the next two sections explain why.
compilingCompiling…
One root, because a function returns one value
Here are two tags side by side in a component's return:
export default function TeaCard() { return ( <h3>Green tea</h3> <p>$3</p> );}The compiler stops on line 4. Picture what it would have to write: each tag is a call, so the return would hold two calls side by side:
return (
_jsx("h3", { children: "Green tea" })
_jsx("p", { children: "$3" })
);Lines 2 and 3 are two values with nothing joining them, and a return gives back exactly one value. There's no JavaScript to write, so the compiler gives up. Different compilers say it differently. Sucrase, in these live examples, says Unterminated regular expression: once the <h3> has ended, it reads the next < as "less than", then tries to read /p> as a regular expression, and gets lost. Babel says it plainly: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?, and TypeScript says JSX expressions must have one parent element.
return gives back one value. Two tags side by side are two values, so the compiler stops (left). A Fragment, <>…</>, holds both in its children, so the component returns one element (right).The fix is one parent around both. A <div> works, and adds a <div> to the page:
export default function TeaCard() { return ( <div> <h3>Green tea</h3> <p>$3</p> </div> );}Lines 3 and 6 open and close the <div>. When you don't want an extra element, for example inside a list or a table where a <div> doesn't belong, use a Fragment: an empty tag, <> and </>.
export default function TeaCard() { return ( <> <h3>Green tea</h3> <p>$3</p> </> );}The result looks the same, but the page holds only the <h3> and the <p>. Here is what the Fragment compiles to:
import {
jsx as _jsx,
Fragment as _Fragment,
jsxs as _jsxs,
} from "react/jsx-runtime";
export default function TeaCard() {
return _jsxs(_Fragment, {
children: [
_jsx("h3", { children: "Green tea" }),
_jsx("p", { children: "$3" }),
],
});
}Line 3 imports Fragment along with jsx and jsxs. Lines 7 to 12 are one call, with React's Fragment as the type and both tags in children: one value for the return. When React meets a Fragment, it puts the children on the page and nothing for the Fragment itself. The long form, <Fragment>…</Fragment> with Fragment imported from react, does the same and can take a key, which the Rendering lists lesson uses.
Attributes become object keys
You saw className="tea-card" become { className: "tea-card" } in the compiled card. Every attribute becomes a key in the props object, and React DOM uses each key to set the matching DOM property on the real element, the way you would in plain JavaScript with element.className = "tea-card" (the Modifying the DOM lesson covers DOM properties). That's where JSX's attribute names come from: they're the DOM properties' names.
export default function SignUp() { return ( <form className="join-box"> <label htmlFor="email">Email</label> <input id="email" type="email" autoComplete="email" maxLength="40" aria-describedby="email-hint" /> <p id="email-hint" className="hint">One email a month.</p> </form> );}Line by line:
- Line 3:
className, because the DOM property forclassiselement.className. The DOM chose that name becauseclassis a reserved word in JavaScript. - Line 4:
htmlFor, the DOM property forfor, which is a reserved word too. Click Email in the result to see it link the label to the box. - Lines 8 and 9: multi-word attributes are camelCase, like their DOM properties:
autoCompleteandmaxLength. So aretabIndex, and SVG'sstrokeWidthforstroke-width. Event props likeonClickare camelCase too; Responding to events covers them. - Line 10:
aria-describedbykeeps its dashes.aria-*anddata-*attributes are written exactly as in HTML.
There's a JavaScript reason too. You'll often take props out with destructuring, and that makes each name a variable. const { className } = props works, but const { class } = props is a SyntaxError, and a name with a dash, like stroke-width, can't be a variable at all.
class and for.What if you write HTML's names anyway? Here the form uses class, for, and tabindex:
export default function SignUp() { return ( <form class="join-box"> <label for="email">Email</label> <input id="email" type="email" tabindex="0" /> </form> );}The page still looks right: React passes the unknown names to the page as attributes. But press Edit & run and read the console. React warns once for each of lines 3 to 5, and names the fix, as in Invalid DOM property `class`. Did you mean `className`? Treat these warnings as bugs. Pick an attribute below to see how JSX spells it, with live checks of why:
<p class="hint">One email a month.</p><p className="hint">One email a month.</p>let class; …React warns: Invalid DOM property `class`. Did you mean `className`?Write className in JSX. It's the name of the DOM property, p.className.
Turning real HTML into JSX
You'll often start from HTML: a design someone handed you, an old page, a snippet from documentation. Here are a tea shop's opening hours and a visit form, with every kind of difference in it:
<!-- The shop's opening hours --><h2 class="title">Opening hours</h2><p>Monday to Friday: 8 to 6<br>Saturday: 9 to 4</p><label for="visit">Plan a visit</label><input id="visit" type="date"><label for="name">Your name</label><input id="name" maxlength="20">Turn it into JSX with five checks, in this order:
- Wrap it all in one root. Here, a Fragment,
<>…</>, since the page doesn't need another element. - Close every tag: the
<br>on line 3 and both inputs. - Rename
class(line 2) toclassNameand eachfor(lines 4 and 6) tohtmlFor. - Write other multi-word attributes in camelCase:
maxlengthon line 7 becomesmaxLength. - Turn the HTML comment on line 1 into a JavaScript comment inside braces,
{/* … */}. JSX has no<!-- -->; the braces, which the next lesson is about, let you put JavaScript, and its comments, in markup.
export default function OpeningHours() { return ( <> {/* The shop's opening hours */} <h2 className="title">Opening hours</h2> <p>Monday to Friday: 8 to 6<br />Saturday: 9 to 4</p> <label htmlFor="visit">Plan a visit</label> <input id="visit" type="date" /> <label htmlFor="name">Your name</label> <input id="name" maxLength="20" /> </> );}Line 3 opens the Fragment and line 11 closes it. Lines 4 to 10 are the HTML's seven lines, each fixed. Line 4 is the comment, which never reaches the page. The two results look the same. For a long page, an HTML-to-JSX converter does these steps for you; it still helps to know what it changes and why. One more HTML habit, a style="…" attribute, needs an object in JSX; the next lesson covers it.
How to read this table: each row is an HTML habit, how JSX writes it, and why.
| In HTML | In JSX | Why |
|---|---|---|
| Several tags side by side | Wrap them in one parent, or in <>…</>. | A function returns one value, so a component returns one element. |
<br>, <input>, <li> left open | Close every tag: <br />, <input />, <li>…</li>. | The compiler must know where each tag's children end. |
class="hint" | className="hint" | React uses the DOM property's name, and class is a reserved word in JavaScript. |
for="email" | htmlFor="email" | The DOM property is htmlFor, because for is a reserved word too. |
tabindex, maxlength, stroke-width | tabIndex, maxLength, strokeWidth | Attributes become object keys, named like DOM properties: camelCase, with no dashes. |
aria-label, data-tea | The same: aria-label, data-tea | These two families keep their dashes in JSX. |
<!-- a note --> | {/* a note */} | JSX has no HTML comments. Braces hold a JavaScript comment instead. |
<br /><p>Tea<br>Cake</p><p className="hint">Hi</p><p class="hint">Hi</p><button aria-label="Close">x</button><input tabindex="0" /><><h3>Tea</h3><p>$3</p></><h3>Tea</h3><p>$3</p><label htmlFor="email">Email</label><li data-tea="green">Green tea</li>
Sort each snippet: would it compile and run with no warning, or does it need a fix?
Common mix-ups
- "JSX is HTML inside JavaScript." No. It looks like HTML, but each tag becomes a function call that returns a plain object. That's why its rules are JavaScript's.
- "The browser runs my JSX." No. A compiler turns it into JavaScript first: your build tool in a project, or Sucrase in this site's live examples.
- "JSX only works with React." No. JSX is a syntax; React is a library. Other libraries use JSX with their own
jsxfunctions, and React works without JSX, throughcreateElement. - "A Fragment adds a wrapper element." No.
<>…</>groups tags into one value and leaves nothing on the page. - "
classNameis a React invention." No. It's the name of the DOM property,element.className, which existed long before React. - "Every attribute is camelCase in JSX." Almost.
aria-*anddata-*keep their dashes. - "Every file with JSX needs
import React." Not since the modern transform: the compiler importsjsxfromreact/jsx-runtimeitself.
Practice exercises
Each exercise shows its code's result under it, or when you press Edit & run. When the result is an error, read its line number first. Try a guess, then use the hints.
How many jsx() or jsxs() calls does this compile to? Use the compiler playground above to check.
const menu = (
<ul className="menu">
<li>Green tea</li>
<li>Mint tea</li>
<li>Lemon cake</li>
</ul>
);import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const menu = _jsxs("ul", {
className: "menu",
children: [
_jsx("li", { children: "Green tea" }),
_jsx("li", { children: "Mint tea" }),
_jsx("li", { children: "Lemon cake" }),
],
});Four tags, four calls: one jsxs for the <ul>, whose three children come as an array, and one jsx for each <li>. This is the real output of TypeScript's compiler, formatted.
Press Edit & run: the result is an error. Fix the two open tags. What do you write in place of <br>?
export default function SignUp() {
return (
<form>
<p>One email a month.<br>New teas first.</p>
<input type="email">
<button>Join</button>
</form>
);
}export default function SignUp() { return ( <form> <p>One email a month.<br />New teas first.</p> <input type="email" /> <button>Join</button> </form> );}<br> and <input> never have children, but the compiler doesn't know that. <br /> and <input type="email" /> close themselves, so the compiler knows where each one ends.
Press Edit & run: two tags side by side stop the compiler. Make the cake card work without adding an element to the page. How many elements does <>…</> add to the page?
export default function CakeCard() {
return (
<h3>Lemon cake</h3>
<p>$4</p>
);
}export default function CakeCard() { return ( <> <h3>Lemon cake</h3> <p>$4</p> </> );}The Fragment makes the two tags one value, jsxs(Fragment, { children: [...] }). The page gets the <h3> and the <p> and nothing else: the Fragment adds 0 elements.
This feedback form works, but React warns four times. Fix every attribute. What does for become?
export default function Feedback() { return ( <form class="join-box"> <label for="note">Tell us what you think</label> <textarea id="note" maxlength="200" tabindex="0" /> <button>Send</button> </form> );}export default function Feedback() { return ( <form className="join-box"> <label htmlFor="note">Tell us what you think</label> <textarea id="note" maxLength="200" tabIndex="0" /> <button>Send</button> </form> );}class becomes className and for becomes htmlFor, the DOM properties' names, and maxlength and tabindex become camelCase, maxLength and tabIndex. The form looked the same before, but now React has nothing to warn about.
Turn this HTML into the TodaysSpecial component:
<!-- Today's special -->
<h3 class="special">Today's special</h3>
<p>Jasmine tea<br>$5 a pot</p>
<label for="cups">Cups</label>
<input id="cups" type="number" maxlength="2">How many elements does your component put on the page?
export default function TodaysSpecial() { // Turn the HTML above into JSX, and return it here return null;}export default function TodaysSpecial() { return ( <> {/* Today's special */} <h3 className="special">Today's special</h3> <p>Jasmine tea<br />$5 a pot</p> <label htmlFor="cups">Cups</label> <input id="cups" type="number" maxLength="2" /> </> );}The Fragment adds nothing and the comment never reaches the page, so the page gets 5 elements: the <h3>, the <p> with its <br> inside, the <label>, and the <input>. The JSX has the same five tags as the HTML, now closed and renamed.
This menu uses no JSX. Add a second list item, Mint tea, by hand. How many list items does the page show now?
import { createElement } from "react"; export default function Menu() { return createElement( "ul", null, createElement("li", null, "Green tea"), );}import { createElement } from "react"; export default function Menu() { return createElement( "ul", null, createElement("li", null, "Green tea"), createElement("li", null, "Mint tea"), );}With createElement, children are extra arguments: the new call on line 8 is the <ul>'s second child. It builds the same elements as this JSX:
<ul>
<li>Green tea</li>
<li>Mint tea</li>
</ul>
Check your understanding
Picture each tag as its jsx() call: most answers follow from it. Press Edit & run on a question's code to check your answer.
Question 1 of 8With the modern JSX transform, what does
<h3>Green tea</h3>compile to?Choose an answer to see the explanation.
Question 2 of 8Why can't a component return two tags side by side, like
<h3>…</h3><p>…</p>?Choose an answer to see the explanation.
Question 3 of 8What does this component put on the page?
Read the code, then predictexport default function TeaCard() { return ( <> <h3>Green tea</h3> <p>$3</p> </> ); }Choose an answer to see the explanation.
Question 4 of 8Which line is valid JSX?
Choose an answer to see the explanation.
Question 5 of 8How do you write HTML's
<label for="email">in JSX?Choose an answer to see the explanation.
Question 6 of 8Which attribute keeps its dashes in JSX?
Choose an answer to see the explanation.
Question 7 of 8What does this code print in the console?
Read the code, then predictfunction TeaCard() { return <p>Green tea</p>; } const card = <TeaCard />; console.log(typeof card.type); export default function App() { return card; }Choose an answer to see the explanation.
Question 8 of 8In a React 19 project, do you need
import React from "react"in a file that only uses JSX?Choose an answer to see the explanation.
Key takeaways
- JSX is markup inside JavaScript. A compiler turns each tag into a
jsx()orjsxs()call fromreact/jsx-runtime, which returns an element: a plain object with atypeandprops. - The classic transform wrote
React.createElementand neededimport React. React 19 needs the modern one;createElementstays for building elements by hand. - A component returns one root, because a function returns one value. Use a Fragment,
<>…</>, to add none. - Close every tag,
<br />and<input />too: the compiler must know where children end. - Attributes become props keys named like DOM properties:
className,htmlFor, camelCase. Onlyaria-*anddata-*keep their dashes.
Remember the one-liner.
Every JSX tag is a function call, so JSX follows JavaScript's rules, not HTML's.
Coming next: JavaScript in JSX with curly braces. It shows how to put values, expressions, and objects like style inside your markup.