Skip to content
aviral gupta

// B2.2 · ~30 min · Beginner

JSX and curly braces

After this lesson you turn HTML into valid JSX, put JavaScript into it with curly braces, and style elements with a style object.

Lesson 2 of 6 in B2 Describing the UI

You will be able to

  • Write JSX by its rules: one root or a Fragment, closed tags, camelCase attributes, aria-* and data-* with dashes
  • Pass strings in quotes and JavaScript in curly braces, as text in a tag or right after =
  • Pass objects with double curlies, such as style={{ backgroundColor: 'black' }}, and read values from an object
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B2.1: a component defined once and used several times. How many <li> elements does TodoList render?

    function Task() {
      return <li>Buy milk</li>;
    }
    
    function TodoList() {
      return (
        <ul>
          <Task />
          <Task />
          <Task />
        </ul>
      );
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. The braces sit inside the quotes. What is the src of the picture?

    const avatar = 'hedy.jpg';
    
    function Avatar() {
      return <img src="{avatar}" alt="Hedy Lamarr" />;
    }
  3. Practice · Activity 3 of 7

    Fill in the style property that gives the heading a black background, as CSS's background-color would.

    <h1 style={{ ____: 'black' }}>Todos</h1>
    <h1 style={{ : 'black' }}>Todos</h1>
  4. Practice · Activity 4 of 7

    Match each HTML attribute to the way you write it in JSX.

  5. Practice · Activity 5 of 7

    What does the style attribute of the <div> say in the page?

    const person = {
      name: 'Hedy Lamarr',
      theme: { backgroundColor: 'black', color: 'pink' }
    };
    
    function TodoHeader() {
      return (
        <div style={person.theme}>
          <h1>{person.name}'s Todos</h1>
        </div>
      );
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Some template languages put variables in double curlies. What does this heading show in React?

    const name = 'Hedy';
    
    function Title() {
      return <h1>{{name}}</h1>;
    }
  7. Apply · Activity 7 of 7

    Mini-task: your own to-do list. In main.jsx, keep a person object with a name and a theme (backgroundColor and color), and an array tasks with three strings. The default export renders a <section> styled with person.theme and with an aria-label, a heading "<name>'s Todos (<count>)", the three tasks in a list, a closed <hr />, and a small note with style={{ fontSize: 12 }}. Read every value with curly braces. Run it and check that React prints no warning.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A to-do list header

The header of a to-do list, in the style of react.dev's examples. The person object holds a name, a picture and a theme; the header reads them all with curly braces, and style={person.theme} passes the theme object on. The SVG check mark uses strokeWidth, the camelCase name, while data-day and aria-label keep their dashes. The note gets an inline style object in double curlies. The snapshot shows what React made of each attribute.

main.jsx

const person = {
  name: 'Hedy Lamarr',
  imageUrl: 'hedy-lamarr.jpg',
  theme: {
    backgroundColor: 'black',
    color: 'pink'
  }
};

const today = 'Monday';

export default function TodoHeader() {
  return (
    <header style={person.theme} data-day={today}>
      <img className="avatar" src={person.imageUrl} alt={person.name} />
      <h1>{person.name}'s Todos for {today}</h1>
      <svg width="24" height="24" aria-label="Done">
        <path d="M4 12l5 5L20 6" stroke="pink" strokeWidth={2} fill="none" />
      </svg>
      <p style={{ fontStyle: 'italic' }}>3 tasks left</p>
    </header>
  );
}

Run it with

npm run dev

Output

<!-- #root -->
<div id="root"><header style="background-color: black; color: pink;" data-day="Monday"><img class="avatar" alt="Hedy Lamarr" src="hedy-lamarr.jpg"><h1>Hedy Lamarr's Todos for Monday</h1><svg width="24" height="24" aria-label="Done"><path d="M4 12l5 5L20 6" stroke="pink" stroke-width="2" fill="none"></path></svg><p style="font-style: italic;">3 tasks left</p></header></div>
  • className became class, strokeWidth became stroke-width, and the style objects became CSS text with dashes.
  • data-day and aria-label are written as in HTML, with dashes, and stay that way.
  • strokeWidth={2} passes a number; the page shows it as the text "2".
  • The closing slashes of <img /> and <path /> are JSX only: the HTML has <img> and a <path></path> pair.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Exercises

Exercise 1 of 2

Convert a pasted form to JSX

Someone pasted the form of a to-do app from an HTML page. It renders, but React prints four warnings. Rewrite the attributes the JSX way, so that React prints none, and keep what they do: the label still belongs to the field, the icon keeps its stroke width of 2, aria-label stays as it is. Then give the Add button a black background with an inline style object.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    Run it and read the warnings: each one names the attribute and the correction.

  2. Hint 2

    class, for, tabindex and stroke-width become className, htmlFor, tabIndex and strokeWidth; aria-label keeps its dash.

  3. Hint 3

    The style is an object in braces: style={{ backgroundColor: 'black' }}.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export default function TodoForm() {
  return (
    <form className="todo-form">
      <label htmlFor="task">New task</label>
      <input id="task" type="text" tabIndex="1" />
      <button type="submit" aria-label="Add task" style={{ backgroundColor: 'black' }}>
        <svg width="16" height="16">
          <path d="M8 2v12M2 8h12" stroke="white" strokeWidth="2" />
        </svg>
      </button>
    </form>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

// Pasted from an HTML page. Make it warning-free JSX, and give the Add
// button a black background with an inline style object.
export default function TodoForm() {
  return (
    <form class="todo-form">
      <label for="task">New task</label>
      <input id="task" type="text" tabindex="1" />
      <button type="submit" aria-label="Add task">
        <svg width="16" height="16">
          <path d="M8 2v12M2 8h12" stroke="white" stroke-width="2" />
        </svg>
      </button>
    </form>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import TodoForm from './main.jsx';

test('The label New task belongs to the text field', () => {
  render(<TodoForm />);
  const input = screen.getByLabelText('New task');
  assert.equal(input.id, 'task', 'the label points at the element with id ' + JSON.stringify(input.id));
});

test('The Add button keeps its aria-label', () => {
  render(<TodoForm />);
  assert.ok(screen.queryByRole('button', {name: 'Add task'}), 'no button is named Add task');
});

test('The Add button has a black background', () => {
  render(<TodoForm />);
  const background = screen.getByRole('button', {name: 'Add task'}).style.backgroundColor;
  assert.equal(background, 'black', 'the button has background-color ' + JSON.stringify(background));
});

test('The icon keeps a stroke width of 2', () => {
  const {container} = render(<TodoForm />);
  const width = container.querySelector('path').getAttribute('stroke-width');
  assert.equal(width, '2', 'the path has stroke-width=' + JSON.stringify(width));
});

test('The form has the CSS class todo-form', () => {
  const {container} = render(<TodoForm />);
  const className = container.querySelector('form').className;
  assert.equal(className, 'todo-form', 'the form has the class ' + JSON.stringify(className));
});

test('React prints no warnings', () => {
  const warnings = [];
  const original = console.error;
  console.error = (...args) => warnings.push(args.join(' '));
  try {
    render(<TodoForm />);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error while rendering');
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Exercise 2 of 2

An avatar styled from an object

TodoHeader shows the object person, but every value is typed in quotes. Read them with curly braces: the picture's src from person.imageUrl and its alt from person.name, and the heading "<name>'s Todos". Make the picture person.imageSize pixels wide and high with a style object, and style the <header> with person.theme. The picture does not load in the runner; the tests check the attributes.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    Run it first: the heading shows the words person.name, because they are text, not JavaScript.

  2. Hint 2

    Replace the quotes with braces: src={person.imageUrl}, and in the heading {person.name}'s Todos.

  3. Hint 3

    Sizes go in double curlies, style={{ width: person.imageSize, height: person.imageSize }}; a number means pixels. The header takes style={person.theme}.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

const person = {
  name: 'Gregorio Y. Zara',
  imageUrl: 'gregorio-zara.jpg',
  imageSize: 90,
  theme: {
    backgroundColor: 'black',
    color: 'pink'
  }
};

export default function TodoHeader() {
  return (
    <header style={person.theme}>
      <img
        className="avatar"
        src={person.imageUrl}
        alt={person.name}
        style={{ width: person.imageSize, height: person.imageSize }}
      />
      <h1>{person.name}'s Todos</h1>
    </header>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

const person = {
  name: 'Gregorio Y. Zara',
  imageUrl: 'gregorio-zara.jpg',
  imageSize: 90,
  theme: {
    backgroundColor: 'black',
    color: 'pink'
  }
};

export default function TodoHeader() {
  // TODO: read every value from person with curly braces, and add the styles.
  return (
    <header>
      <img className="avatar" src="person.imageUrl" alt="person.name" />
      <h1>person.name's Todos</h1>
    </header>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import TodoHeader from './main.jsx';

test("The heading reads Gregorio Y. Zara's Todos", () => {
  render(<TodoHeader />);
  const heading = screen.getByRole('heading', {level: 1}).textContent;
  assert.equal(heading, "Gregorio Y. Zara's Todos", 'the heading reads ' + JSON.stringify(heading));
});

test('The picture reads src and alt from person', () => {
  render(<TodoHeader />);
  const img = screen.getByRole('img');
  assert.equal(img.getAttribute('src'), 'gregorio-zara.jpg', 'the picture has src=' + JSON.stringify(img.getAttribute('src')));
  assert.equal(img.getAttribute('alt'), 'Gregorio Y. Zara', 'the picture has alt=' + JSON.stringify(img.getAttribute('alt')));
});

test('The picture is 90px wide and high, from person.imageSize', () => {
  render(<TodoHeader />);
  const {width, height} = screen.getByRole('img').style;
  assert.equal(width, '90px', 'the picture has width ' + JSON.stringify(width));
  assert.equal(height, '90px', 'the picture has height ' + JSON.stringify(height));
});

test('The header uses person.theme as its style', () => {
  render(<TodoHeader />);
  const {backgroundColor, color} = screen.getByRole('banner').style;
  assert.equal(backgroundColor, 'black', 'the header has background-color ' + JSON.stringify(backgroundColor));
  assert.equal(color, 'pink', 'the header has color ' + JSON.stringify(color));
});

test('React prints no warnings', () => {
  const warnings = [];
  const original = console.error;
  console.error = (...args) => warnings.push(args.join(' '));
  try {
    render(<TodoHeader />);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error while rendering');
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Common mistakes

Writing style as a string

export default function Title() {
  return <h1 style="color: red">Todos</h1>;
}

What React prints

The `style` prop expects a mapping from style properties to values, not a string.

Why, and the fix

In React, style takes an object, not CSS text, and a string stops the render: the run prints Uncaught Error with this message. Write the object in double curlies, with camelCase names: <h1 style={{ color: 'red' }}>. For background-color, write backgroundColor.

Leaving <img> open, as HTML allows

export default function Avatar() {
  return (
    <div>
      <img src="hedy.jpg" alt="Hedy Lamarr">
    </div>
  );
}

What React prints

error TS17008: JSX element 'img' has no corresponding closing tag.

Why, and the fix

HTML lets <img> stay open, but JSX requires every tag to be closed, so the JSX transform stops and nothing runs. The full line starts with the position, main.jsx(4,8). Close it with a slash: <img src="hedy.jpg" alt="Hedy Lamarr" />. The same goes for <br />, <hr /> and <input />.

Writing for on a label

export default function TaskField() {
  return (
    <>
      <label for="task">Task</label>
      <input id="task" />
    </>
  );
}

What React prints

Invalid DOM property `for`. Did you mean `htmlFor`?

Why, and the fix

for is a reserved word in JavaScript, so JSX names the attribute htmlFor, after the DOM property. React 19 still puts for="task" on the label, but warns, and the course runner counts any warning as a failed run. Write <label htmlFor="task">. React gives the same kind of correction for tabindex (tabIndex) and stroke-width (strokeWidth).

React in the browser: React 19.3.0 (development build) and React Testing Library, MIT, served from this site; JSX is compiled by TypeScript 6.0.3, Apache-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

JSX: markup and logic together, with stricter rules

JSX lets you write HTML-like markup inside JavaScript, so a component keeps its logic and its markup in one place. Pasted HTML needs three changes. Return one root element, or wrap siblings in a Fragment <>…</>, which leaves no trace in the page. Close every tag: <img /> and <br /> end with a slash, <li> needs </li>. Write most attributes in camelCase: className, htmlFor, tabIndex, strokeWidth. Only aria-* and data-* keep their dashes. A wrong name still renders, but React prints a warning with the correction.

Quotes pass strings, braces pass JavaScript

An attribute in quotes is a string: alt="Hedy Lamarr". Curly braces open a window to JavaScript: src={avatar} reads the variable avatar, and any expression works, such as {tasks.length} or a call like {formatDate(today)}. Braces work in exactly two places: as text inside a tag, <h1>{name}'s Todos</h1>, and right after the = of an attribute. src="{avatar}" passes the text {avatar}, and a tag name in braces, <{tag}>, does not compile.

Double curlies are an object in braces

To pass an object, write it inside the JSX braces, which gives two pairs: style={{ backgroundColor: 'black', color: 'pink' }}. The style object uses camelCase property names, so CSS's background-color becomes backgroundColor, and a plain number such as fontSize: 24 means 24px. A style string, style="color: red", is an error in React. You can also keep values in an object, person.theme, and pass it: style={person.theme}. But an object cannot be shown as text: {person} inside a tag is an error; show {person.name}.

Sources

Last reviewed October 5, 2026