Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~24 min· topic 1 of 6

Topic 11.1

className, style and Plain CSS

In one line

React styles elements with two props: className, a string of CSS class names you build from state, and style, an object of inline styles for truly dynamic values; ordinary CSS files and CSS variables do the rest.

Think of it like this

A school uniform and a name badge. Every pupil wears the same uniform (a class: one shared look written once), and on top of it each pupil pins their own name badge (an inline style: one value that belongs only to this person). You would never sew a whole new uniform for each child, and you would never print one name badge for the whole school. In React, className is the uniform and style is the badge.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

className
The JSX name for the HTML class attribute. Takes one string of space-separated class names.
Global stylesheet
A normal .css file whose rules apply to the whole page.
Conditional class
A class that is added or left out depending on state or props, like task--done when a task is finished.
clsx
A tiny library function that joins class names and drops the ones whose condition is false.
BEM
A naming habit (Block__Element--Modifier) that keeps global class names from clashing.
Inline style
Styles written on one element through the style prop, as a JavaScript object.
camelCase
Writing words joined with capitals instead of dashes: backgroundColor instead of background-color.
Unitless property
A CSS property where a plain number is valid, such as opacity: 0.5, so React doesn't add px.
CSS variable
A custom property like --accent: teal that other rules read with var(--accent). Children inherit it.

Step by step

01Load a global stylesheet once

In a Vite project, import the CSS file from your entry file. Vite injects it in development and turns it into a hashed .css file in the production build. Every component can now use its classes.

src/main.jsxwhole filejsx
import { createRoot } from "react-dom/client";
import "./index.css"; // global styles for the whole app
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(<App />);

02Give elements classes with className

Write the class names the stylesheet defines. A BEM-style name (task, task__label, task--done) tells a reader which component the class belongs to and makes accidental clashes rarer.

src/index.csswhole filecss
.task { padding: 8px 12px; border-left: 4px solid transparent; }
.task--done { color: #868e96; text-decoration: line-through; }
.task--urgent { border-left-color: #e03131; }

03Build the class string from state

The class list is computed on every render from the current data, exactly like the text is. With two or more conditions, an object in clsx reads better than nested ternaries.

Task.jsxwhole filejsx
import clsx from "clsx";

function Task({ text, done, urgent }) {
  return (
    <li className={clsx("task", { "task--done": done, "task--urgent": urgent && !done })}>
      {text}
    </li>
  );
}

04Use style only for the value that really changes

A progress bar's width is a different number on every render; writing a class for each percentage is impossible. That is the job style is for. Note the double braces and the camelCase names.

Progress.jsxwhole filejsx
function Progress({ value }) {
  return (
    <div className="progress">
      <div className="progress__fill" style={{ width: value + "%" }} />
    </div>
  );
}

05Pass colours and sizes as CSS variables

Instead of style={{ background: color }} on the inner element, set a variable on the outer one and keep the real rule in CSS. Now a :hover rule or a dark-mode media query in the stylesheet can still adjust it.

Meter.jsxwhole filejsx
// CSS: .meter__fill { background: var(--meter-color, teal); transition: width 0.2s; }
function Meter({ value, color }) {
  return (
    <div className="meter" style={{ "--meter-color": color }}>
      <div className="meter__fill" style={{ width: value + "%" }} />
    </div>
  );
}
Pass colours and sizes as CSS variablesdiagram
Rendering diagram…

Try it yourself

  1. 1

    Add a third condition

    In the task example, add a today: true field to "Call Ravi" and a "task--today": today entry in the clsx object, with a CSS rule .task--today { font-weight: 700; }. Predict the logged class string for "Call Ravi" before you run it.

  2. 2

    Move a colour into the stylesheet

    In the meter example, delete color="#e8590c" from the second meter. Predict its colour. (Teal: var(--meter-color, teal) uses its fallback when the variable is missing.)

  3. 3

    Find a unitless property

    Add zIndex: 2 and fontSize: 14 to the paragraph's style. Which one gets px in the logged attribute? Check your guess against the console.

Code & diagrams

Global classes, conditional classes and a tiny clsx New tab

The stylesheet (CSS tab) is global: it styles every `.task` on the page. Tick "Pay rent" and watch the console: the class string is recomputed and the red urgent border goes away because the condition is `urgent && !done`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Pay rent -> "task task--urgent"
Buy milk -> "task task--done"
Call Ravi -> "task"
style for dynamic values, CSS variables from props New tab

The console shows the exact `style` attributes React wrote. The second meter's fill is orange although its JSX never mentions a background: the rule `var(--meter-color)` in the CSS tab read the variable. Hover a meter: the `:hover` rule still works because the colour lives in CSS.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

--meter-color: #0b7285;
width: 40%;
margin-top: 8px; opacity: 0.7; line-height: 1.4;
rgb(232, 89, 12)
Where each kind of styling belongsdiagram
Rendering diagram…

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

Write style as a string, like in HTML

In the meter example, change the paragraph to <p style="margin-top: 8px">.

terminal
$ Console
── what you'll see ──
Uncaught Error: The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX.
An error occurred in the <p> component.

Break #2

Use HTML names: class and background-color

Write <p class="note" style={{ "background-color": "yellow" }}>.

terminal
$ Console
── what you'll see ──
Invalid DOM property `class`. Did you mean `className`?
Unsupported style property background-color. Did you mean backgroundColor?

Myth vs fact

Myth

Inline styles are faster because there is no stylesheet to load.

Fact

The browser has to parse and apply every inline style separately, and React has to compare style objects on every render. A class is one short string. For repeated elements, classes are cheaper and far easier to change.

Myth

You can't use :hover or media queries with dynamic React values.

Fact

Put the value in a CSS variable through style and write the :hover or @media rule in the stylesheet using var(--name).

Myth

class doesn't work in React.

Fact

React 19 does set the attribute and warns in development. Use className anyway: it is the documented prop and what every tool expects.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    React updates styles per property: if only width changed between renders, it sets element.style.width and leaves the rest alone. Custom properties (keys starting with --) are set with style.setProperty, which is why their dashes and case are kept as written.

  • ▸

    With TypeScript, style={{ "--accent": color }} fails because CSSProperties has no key --accent. Common fixes: cast (as React.CSSProperties) or augment the CSSProperties interface with an index signature for keys that start with -- (Phase 12).

  • ▸

    The cascade still applies to React apps: import order of CSS files decides which equal-specificity rule wins, and code-split routes can load CSS in a different order in production than in development. Cascade layers (@layer reset, base, components, utilities;) make the order explicit and are how Tailwind v4 organises its own CSS.

  • ▸

    Server rendering (Phase 20) sends className and style as plain HTML attributes, so the page looks right before JavaScript loads. Styles computed in effects or from window sizes don't exist on the server and cause a visible jump; prefer CSS media queries and container queries for layout.

Remember this

  1. 1

    className is how JSX writes the HTML class attribute. JSX is JavaScript, and class is a reserved word in JavaScript (it starts a class declaration), so React uses the DOM property name instead: <li className="task">. If you write class, React 19 still sets the attribute but prints a warning in development. className takes one string; several classes are separated by spaces: className="task task--done".

  2. 2

    A global stylesheet is an ordinary .css file loaded once for the whole page. In a Vite project you write import "./index.css"; at the top of main.jsx and Vite adds it to the page. Every rule in it applies everywhere, which is both its strength (one place for the look of the whole app) and its danger: two components that both use a class called .title will style each other. Teams fight this with naming rules such as BEM (Block, Element, Modifier: .card, .card__title, .card--featured), or with the scoped approaches in the next topics. The cascade and specificity rules that decide which rule wins are in the CSS course (/css, Phase 2).

  3. 3

    Conditional classes are class names that depend on state or props. Because className is just a string, you build it with JavaScript during render: a ternary (done ? "task task--done" : "task"), a template literal, or an array joined with spaces. This gets messy with three or four conditions, so most projects use clsx (or its older twin classnames): a tiny function that takes strings, arrays and objects like { "task--done": done } and returns only the class names whose value is truthy. You'll write your own in the first live example; the real package is about 240 bytes.

  4. 4

    style takes an object, not a string: style={{ width: value + "%" }}. The outer braces mean "a JavaScript value here", the inner braces are the object. Property names are camelCase (backgroundColor, not background-color), because they map to the DOM's element.style properties. When a value is a plain number, React adds px for you (marginTop: 8 becomes margin-top: 8px), except for unitless properties such as opacity, zIndex, lineHeight, flexGrow and fontWeight, where a number already makes sense. A value the browser can't parse (like padding: "4", which has no unit) is silently dropped.

  5. 5

    Use style for values that are truly dynamic: a width from a slider, a position from a drag, a colour picked by the user. Use classes for everything else. Inline styles can't express :hover, :focus-visible, media queries or ::before, they win over almost every stylesheet rule (only !important beats them), and long style objects scatter the design across components so nobody can change it in one place.

  6. 6

    CSS variables (custom properties, names starting with --) are the bridge between the two. Set the value from a prop with style={{ "--meter-color": color }}, and write every real rule in the stylesheet: .meter__fill { background: var(--meter-color); }. The CSS keeps :hover, transitions and media queries, while React feeds in just the number or colour that changes. Variables are also inherited, so one --accent on a parent themes every child (the CSS course's theming topic, /css Phase 11, goes deeper).

Explain it without notes

01

Why does JSX use className instead of class, and what happens if you write class?

02

When would you use the style prop instead of a class? Give two examples of each.

03

What does a helper like clsx do, and why do teams use it?

04

How do CSS variables help when a style must come from props but still needs :hover or a media query?

Practice

01

Write a Badge component that takes tone ("info", "success" or "danger") and outline (boolean) and renders a <span> with classes badge, badge--<tone> and, when outline is true, badge--outline.

02

Write an Avatar component that takes size (a number) and ring (a colour) and renders a circle of that size whose border colour is set through a CSS variable --ring.

Trade-offs

  • ↔

    Global CSS is simple and fast, with no tooling, but names can clash as the app grows. Naming rules like BEM help; scoped approaches (CSS Modules, Tailwind, CSS-in-JS) remove the problem at the cost of tooling or a new way of writing styles.

  • ↔

    Inline style is the most direct way to bind a value, but it can't do pseudo-classes or media queries and it scatters design decisions. CSS variables keep the dynamic value and the full power of CSS.

Done when you can

  • I can style elements with className and a global stylesheet.

  • I can build conditional class names with a ternary or clsx.

  • I can write a style object with camelCase keys and know when React adds px.

  • I can pass a value from props into CSS through a CSS variable.

  • I can explain when to choose a class and when to choose style.