JSX in Depth

What JSX compiles to, expressions in braces, and the rules that trip people up.

What you'll learn

  • That JSX is a function call in disguise
  • How to put values and expressions into markup
  • The attribute names that differ from HTML, and why
  • Fragments, self-closing tags, and comments

JSX is not HTML

JSX looks like markup, but it is JavaScript. A build step turns every tag into a function call. These two are the same thing:

const element = <p className="intro">Hello</p>;

// compiles to roughly:
const element = jsx("p", { className: "intro", children: "Hello" });

That is the whole trick, and two useful facts follow from it.

First, a JSX expression is a value. You can store it in a variable, put it in an array, return it from a function, or pass it as an argument:

export default function App() {
  const heading = <h1 style={{ fontSize: 20 }}>Stored in a variable</h1>;
  const items = [<li key="a">first</li>, <li key="b">second</li>];

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      {heading}
      <ul>{items}</ul>
    </div>
  );
}

Second, it is not a string. There is no HTML parsing at runtime, and values you interpolate are escaped, so JSX does not expose you to injection the way innerHTML does.

Braces take an expression

Curly braces switch from markup back into JavaScript. Anything that evaluates to a value is allowed:

export default function App() {
  const user = { first: "Ada", last: "Lovelace" };
  const now = new Date(2026, 0, 1);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p>{user.first + " " + user.last}</p>
      <p>{user.first.toUpperCase()}</p>
      <p>{2 + 2} items</p>
      <p>{now.getFullYear()}</p>
    </div>
  );
}

The word expression is doing real work there. An expression produces a value; a statement does not. So if, for, and switch cannot go inside braces — you either compute the value before the return, or use a ternary, which is covered in Conditional Rendering.

Attributes take expressions too

Use braces instead of quotes to pass a value rather than a literal string:

export default function App() {
  const size = 24;
  const color = "rebeccapurple";

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p style={{ fontSize: size, color: color }}>Styled from variables</p>
      <input defaultValue="editable" disabled={false} />
      <img src="https://placehold.co/60x40" alt="" width={60} height={40} />
    </div>
  );
}

Note style takes an object, not a string — which is why it has two sets of braces: one to enter JavaScript, one for the object literal. Its properties are camelCased (fontSize, not font-size).

Attribute names that differ

JSX attributes are named after DOM properties, not HTML attributes. The ones you will hit immediately:

| HTML | JSX | | --- | --- | | class | className | | for | htmlFor | | tabindex | tabIndex | | onclick | onClick |

class and for are reserved words in JavaScript, which is the reason for the first two. The rest is a consistent rule: multi-word attributes are camelCased.

One parent element

A component returns one value, so JSX must have a single root. Two adjacent elements are a syntax error.

When you do not want a wrapper element in the output, use a fragment — written <>...</>:

function Pair() {
  // A fragment groups these without adding a DOM node.
  return (
    <>
      <dt style={{ fontWeight: 600 }}>Fragment</dt>
      <dd style={{ margin: "0 0 8px" }}>Groups without wrapping</dd>
    </>
  );
}

export default function App() {
  return (
    <dl style={{ fontFamily: "sans-serif", padding: 16 }}>
      <Pair />
    </dl>
  );
}

That matters for layout: an extra <div> inside a grid or flex container can break it. A fragment leaves no trace in the DOM.

Closing tags and comments

Every element must be closed. Tags with no children close themselves — <br />, <img />, <input /> — including your own components: <Greeting />.

Comments inside JSX go in braces, because they are JavaScript:

<div>
  {/* This is a comment in JSX */}
  <p>Visible</p>
</div>

Common mistakes

  • Using class. It is className.
  • Returning two elements. Wrap them in a fragment.
  • Putting an if in braces. Braces take an expression; compute first.
  • style="color: red". style takes an object: style={{ color: "red" }}.
  • Forgetting to close a tag. <img> must be <img />.

Exercise

This component renders, but React is complaining in the console about two attribute mistakes: it uses class, and its style key is written the CSS way rather than the JavaScript way.

Fix both. Then split the greeting into an <h1> for the heading and a <p> for the sentence — without adding a wrapper element, which is what fragments are for.

export default function App() {
  const name = "Ada";

  return (
    <div class="greeting" style={{ "font-size": "20px" }}>
      Hello, {name}
    </div>
  );
}