Runnable lesson · press Try it on the examples to edit and run them here.
Interfaces change appearance with state: a button is active, a field is invalid, a card is selected. In React you do this by choosing class names from state or props, and letting CSS do the styling.
Ternary for one or two cases
function App() {
const [dark, setDark] = React.useState(false);
return (
<div>
<style>{`
.panel { padding: 16px; border-radius: 8px; }
.panel--light { background: #f4f4f4; color: #111; }
.panel--dark { background: #222; color: #fff; }
`}</style>
<div className={dark ? "panel panel--dark" : "panel panel--light"}>
Dark mode {dark ? "on" : "off"}
</div>
<button onClick={() => setDark(!dark)}>Toggle</button>
</div>
);
}
(The <style> element keeps the CSS in the same file for the playground. In a project the CSS lives in its own file.)
A base class plus modifiers
Keep the always-on class, and add modifier classes conditionally. A small helper keeps this readable:
// Join the truthy names with a space
function cx(...names) {
return names.filter(Boolean).join(" ");
}
function Tag({ label, selected, onClick }) {
return (
<button
type="button"
onClick={onClick}
aria-pressed={selected}
className={cx("tag", selected && "tag--selected")}
>
{selected ? "Selected" : label}
</button>
);
}
function App() {
const [selected, setSelected] = React.useState(false);
return (
<div>
<style>{`
.tag { padding: 4px 12px; border: 1px solid #1a5276; border-radius: 999px; background: #fff; color: #1a5276; cursor: pointer; }
.tag--selected { background: #1a5276; color: #fff; }
`}</style>
<p>Save</p>
<Tag label="Select" selected={selected} onClick={() => setSelected(!selected)} />
</div>
);
}
cx("tag", false) gives "tag"; cx("tag", true && "tag--selected") gives "tag tag--selected". The popular library clsx (or classnames) offers the same helper with more options, if you install it in a project.
Why not build strings by hand?
className={"btn " + (primary ? "btn--primary" : "") + " " + (large ? "btn--large" : "")}
This leaves extra spaces and is hard to read. The helper avoids both.
Variants through an object lookup
When a prop has a fixed set of values, map each to its class:
const variantClass = {
info: "badge badge--info",
warning: "badge badge--warning",
error: "badge badge--error",
};
function Badge({ variant = "info", children }) {
return <span className={variantClass[variant]}>{children}</span>;
}
function App() {
return (
<div style={{ display: "flex", gap: 8 }}>
<style>{`
.badge { padding: 2px 10px; border-radius: 999px; font-size: 13px; }
.badge--info { background: #d6eaf8; color: #154360; }
.badge--warning { background: #fcf3cf; color: #7d6608; }
.badge--error { background: #fadbd8; color: #78281f; }
`}</style>
<Badge>Info</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="error">Error</Badge>
</div>
);
}
A lookup makes the allowed values obvious, and a missing key shows up as undefined. With TypeScript, the keys can be checked at compile time.
Data attributes as an alternative
Instead of many class names, set a data attribute and style it in CSS. It keeps JSX short, and CSS can combine states:
function App() {
const states = ["idle", "loading", "done"];
const [i, setI] = React.useState(0);
const status = states[i];
return (
<div>
<style>{`
.status { padding: 8px 12px; border-radius: 6px; }
.status[data-status="idle"] { background: #eee; }
.status[data-status="loading"] { background: #fdebd0; }
.status[data-status="done"] { background: #d5f5e3; }
`}</style>
<p className="status" data-status={status}>Status: {status}</p>
<button onClick={() => setI((i + 1) % states.length)}>Next</button>
</div>
);
}
State that CSS already knows
Do not recreate what CSS can do: :hover, :focus-visible, :disabled, :checked, :has() and :invalid need no React state. Use state only for things CSS cannot know, such as “this card is selected”.
Accessibility reminder
Colour and class changes must not be the only signal. For a toggle, also set aria-pressed or aria-expanded as in the Tag example, so assistive technology knows the state.
Key points
- Choose class names from state and props; let CSS do the styling.
- Use a base class plus modifiers, a tiny
cxhelper or an object lookup. - Prefer CSS pseudo-classes over state where possible, and add ARIA attributes for state.