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
classattribute. Takes one string of space-separated class names. - Global stylesheet
- A normal
.cssfile whose rules apply to the whole page. - Conditional class
- A class that is added or left out depending on state or props, like
task--donewhen 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
styleprop, as a JavaScript object. - camelCase
- Writing words joined with capitals instead of dashes:
backgroundColorinstead ofbackground-color. - Unitless property
- A CSS property where a plain number is valid, such as
opacity: 0.5, so React doesn't addpx. - CSS variable
- A custom property like
--accent: tealthat other rules read withvar(--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.
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.
.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.
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.
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.
// 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>
);
}Try it yourself
- 1
Add a third condition
In the task example, add a
today: truefield to "Call Ravi" and a"task--today": todayentry in theclsxobject, with a CSS rule.task--today { font-weight: 700; }. Predict the logged class string for "Call Ravi" before you run it. - 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
Find a unitless property
Add
zIndex: 2andfontSize: 14to the paragraph'sstyle. Which one getspxin the logged attribute? Check your guess against the console.
Code & diagrams
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
Console
Nothing logged yet.Expected console output
Pay rent -> "task task--urgent"
Buy milk -> "task task--done"
Call Ravi -> "task"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
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)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">.
Break #2
Use HTML names: class and background-color
Write <p class="note" style={{ "background-color": "yellow" }}>.
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
widthchanged between renders, it setselement.style.widthand leaves the rest alone. Custom properties (keys starting with--) are set withstyle.setProperty, which is why their dashes and case are kept as written. - ▸
With TypeScript,
style={{ "--accent": color }}fails becauseCSSPropertieshas no key--accent. Common fixes: cast (as React.CSSProperties) or augment theCSSPropertiesinterface 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
classNameandstyleas plain HTML attributes, so the page looks right before JavaScript loads. Styles computed in effects or fromwindowsizes don't exist on the server and cause a visible jump; prefer CSS media queries and container queries for layout.
Remember this
- 1
classNameis how JSX writes the HTMLclassattribute. JSX is JavaScript, andclassis a reserved word in JavaScript (it starts a class declaration), so React uses the DOM property name instead:<li className="task">. If you writeclass, React 19 still sets the attribute but prints a warning in development.classNametakes one string; several classes are separated by spaces:className="task task--done". - 2
A global stylesheet is an ordinary
.cssfile loaded once for the whole page. In a Vite project you writeimport "./index.css";at the top ofmain.jsxand 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.titlewill 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
Conditional classes are class names that depend on state or props. Because
classNameis 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 twinclassnames): 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
styletakes 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, notbackground-color), because they map to the DOM'selement.styleproperties. When a value is a plain number, React addspxfor you (marginTop: 8becomesmargin-top: 8px), except for unitless properties such asopacity,zIndex,lineHeight,flexGrowandfontWeight, where a number already makes sense. A value the browser can't parse (likepadding: "4", which has no unit) is silently dropped. - 5
Use
stylefor 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!importantbeats them), and longstyleobjects scatter the design across components so nobody can change it in one place. - 6
CSS variables (custom properties, names starting with
--) are the bridge between the two. Set the value from a prop withstyle={{ "--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--accenton a parent themes every child (the CSS course's theming topic,/cssPhase 11, goes deeper).
Explain it without notes
Why does JSX use className instead of class, and what happens if you write class?
When would you use the style prop instead of a class? Give two examples of each.
What does a helper like clsx do, and why do teams use it?
How do CSS variables help when a style must come from props but still needs :hover or a media query?
Practice
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.
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
styleis 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
classNameand a global stylesheet.I can build conditional class names with a ternary or
clsx.I can write a
styleobject with camelCase keys and know when React addspx.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.