Styling React Components
React does not ship its own styling system. It renders plain HTML elements, so you style those elements with the same tools you already know: CSS files, CSS classes, and inline styles. The main thing React changes is how you attach that styling from JSX — and it also unlocks a few new techniques, like CSS Modules and computed inline styles, that fit naturally into a component-based mental model. This lesson covers every mainstream way to style a React component and when to reach for each one.
Overview / How it works
Because JSX compiles down to React.createElement calls, and those calls eventually produce real DOM nodes, styling a React element is really just setting DOM attributes and properties. There is no special React CSS engine running at render time. What React does do differently from plain HTML is use className instead of class, because class is a reserved word in JavaScript (it’s used to define ES6 classes) and JSX attributes ultimately become JavaScript object properties passed to createElement.
There are three broad approaches, and most real apps use a mix of all three:
- Plain CSS files imported into a component file. The CSS is global — once imported anywhere in the app, its rules apply everywhere, exactly like a traditional stylesheet.
- CSS Modules — files named
*.module.css. Your build tool (Vite, Create React App, Next.js, etc.) rewrites every class name in that file into a unique, hashed name at build time, so styles are automatically scoped to the component that imports them and can never collide with another component’s class of the same name. - Inline styles — a JavaScript object passed to the
styleprop, where property names are camelCased (backgroundColorinstead ofbackground-color) and values are strings or numbers. These are computed at render time, so they’re ideal for values that depend on state or props, but they can’t express pseudo-classes (:hover), pseudo-elements, or media queries.
None of these approaches change how React renders. When a component re-renders, React figures out the new set of DOM attributes (including className and style) it wants for each element, diffs them against what’s currently in the DOM, and only touches the properties that actually changed. A style object is just a normal piece of render output — if you create a brand-new object literal on every render (as in the examples below), that’s fine; it doesn’t cause extra DOM writes because React still compares the resulting style values, not object identity, before touching the actual CSSOM.
Syntax
The three approaches use different syntax on the JSX side:
// 1. Plain CSS class
<div className="card">...</div>
// 2. CSS Module class (styles is the imported object)
<div className={styles.card}>...</div>
// 3. Inline style object
<div style={{ padding: "16px", color: "#333" }}>...</div>
| Part | Meaning |
|---|---|
className |
The JSX prop that sets an element’s CSS class (or classes, space-separated). Always a string. |
styles.card |
Property access on the object a CSS Module file exports; its value is the build-time-generated unique class name, e.g. "_card_1x2y3". |
style={{ ... }} |
Note the double braces: the outer {} is the JSX expression slot, the inner {} is a plain JS object literal. Keys are camelCase CSS properties. |
Examples
Example 1: A plain CSS stylesheet
/* Button.css */
.button {
background-color: #3b82f6;
color: white;
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
}
.button:hover {
background-color: #2563eb;
}
import "./Button.css";
function Button({ children, onClick }) {
return (
<button className="button" onClick={onClick}>
{children}
</button>
);
}
export default Button;
This renders a blue, rounded button. Importing "./Button.css" has a side effect: the build tool injects that CSS into the page once, and from then on any element with className="button" anywhere in the app picks up those rules. This is the simplest approach and works exactly like styling plain HTML, but the class name button is global — a second, unrelated component that also defines a .button class will collide with this one.
Example 2: Dynamic className based on state
import { useState } from "react";
import "./ToggleButton.css";
function ToggleButton() {
const [active, setActive] = useState(false);
return (
<button
className={`toggle-button ${active ? "toggle-button--active" : ""}`}
onClick={() => setActive(!active)}
>
{active ? "ON" : "OFF"}
</button>
);
}
export default ToggleButton;
Renders a pill-shaped button reading “OFF”. Clicking it flips the active state to true, which triggers a re-render; the template literal now includes the extra toggle-button--active class, the button turns green, and the label becomes “ON”. This pattern — building a className string with a template literal and a ternary — is the most common way to express conditional styling without any extra library.
Example 3: CSS Modules for automatic scoping
/* Card.module.css */
.card {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 8px;
}
import styles from "./Card.module.css";
function Card({ title, children }) {
return (
<div className={styles.card}>
<h3 className={styles.title}>{title}</h3>
<p>{children}</p>
</div>
);
}
export default Card;
Renders a bordered, shadowed card with a bold title. Because the file is named Card.module.css, the build tool treats .card and .title as local names and rewrites them into something like ._card_a1b2c and ._title_d4e5f in the compiled CSS and in the styles object you import. So styles.card at runtime is not the string "card" — it’s the unique hashed name. Another component can define its own .card class in its own module and the two will never clash.
Example 4: Inline styles for per-render computed values
function ProgressBar({ percent }) {
const barStyle = {
width: `${percent}%`,
height: "12px",
backgroundColor: percent < 50 ? "#f59e0b" : "#22c55e",
borderRadius: "6px",
transition: "width 0.3s ease",
};
return (
<div style={{ backgroundColor: "#e2e8f0", borderRadius: "6px" }}>
<div style={barStyle} />
</div>
);
}
export default ProgressBar;
Given <ProgressBar percent={72} />, this renders a light-gray track containing a green inner bar that is 72% wide. Because width and backgroundColor depend directly on the percent prop, they cannot be expressed as a static CSS class — the value only exists once the component actually renders with real data. This is exactly the situation inline styles are meant for: values computed from props or state, on every render.
How it works step by step
- On mount: React renders the component function, evaluates the JSX (including any
classNamestring orstyleobject), and creates real DOM nodes with those attributes already set —classin the actual DOM, and individual CSS properties on the node’sstyleobject. - On a state or prop update: the component function runs again, producing a new element tree. React compares the new
classNamestring andstyleobject against what’s currently on the DOM node. IfclassNamechanged (like in the toggle button example), React updates theclassattribute. If individual style properties changed, React updates only those CSS properties on the node — it does not recreate the node or reset unrelated styles. - CSS files and CSS Modules are handled entirely at build time by your bundler, not by React at runtime. By the time your app runs in the browser, a
.module.cssimport has already become a plain JS object of string class names; there is no CSS-in-JS engine running on every render. - On unmount: React removes the DOM node entirely, which removes its inline styles and classes along with it. Global CSS from an imported stylesheet stays loaded in the page (it doesn’t get “unmounted”) since it’s not tied to a specific component instance.
Common Mistakes
Mistake 1: Using class instead of className
function Bad() {
return <div class="box">Hello</div>;
}
class is a reserved JavaScript keyword, so React’s JSX transform never accepted it as a prop name — using it produces a console warning and the class is not applied as expected in the underlying DOM element the way you’d want. Always use className:
function Good() {
return <div className="box">Hello</div>;
}
Mistake 2: Kebab-case keys in an inline style object
const style = {
"background-color": "red",
"font-size": "14px",
};
function Warning() {
return <p style={style}>Careful!</p>;
}
React applies inline styles by setting each key directly on the DOM node’s style object, e.g. node.style.backgroundColor = "red". The style object only recognizes camelCased property names; assigning to a hyphenated string key like "background-color" silently does nothing, so the paragraph stays unstyled. Use camelCase:
const style = {
backgroundColor: "red",
fontSize: "14px",
};
Mistake 3: Concatenating conditional class names without a separator
function Button({ active }) {
return (
<button className={"btn" + (active ? "active" : "")}>
Click me
</button>
);
}
When active is true, this produces the single string "btnactive" instead of two space-separated classes, so neither the .btn nor an .active CSS rule matches anything. Always include the space explicitly, usually with a template literal:
function Button({ active }) {
return (
<button className={`btn ${active ? "active" : ""}`}>
Click me
</button>
);
}
Best Practices
- Default to
classNamewith CSS or CSS Modules for anything reusable — it lets you use:hover,:focus, media queries, and animations, none of which inline styles support. - Reach for CSS Modules once your app has more than a handful of components, so class names never collide across files without you having to invent unique names by hand.
- Reserve inline
styleobjects for values that are genuinely computed per render from props or state, like a progress bar width or a color interpolated from data. - Name a CSS Module file after its component and keep them side by side, e.g.
Card.jsxandCard.module.css, so the relationship is obvious. - For more than two or three conditional classes, consider a small helper like the
clsxpackage instead of manual ternaries and template literals — it keeps the logic readable. - Never mutate a style object in place between renders; build a fresh object (or fresh values) from current props/state each time, the same way you avoid mutating state.
Practice Exercises
- Build a
Badgecomponent that accepts astatusprop of"success","error", or"warning"and applies a different CSS class for each, using a lookup object that maps status to class name. - Convert the
Buttoncomponent from Example 1 to use a CSS Module instead of a global stylesheet. Then create a second, unrelated component that also defines a.buttonclass and confirm the two no longer conflict. - Build an
Avatarcomponent that accepts asizeprop (a number, in pixels) and uses an inline style object to setwidth,height, andborderRadiusto make a perfect circle, while the border color still comes from a CSS class.
Summary
- React has no built-in styling engine; it renders plain DOM elements, so ordinary CSS techniques still apply.
- Use
className, neverclass, to attach CSS classes from JSX. - Plain CSS imports are global; CSS Modules (
*.module.css) are automatically scoped by hashing class names at build time and are accessed as properties on the importedstylesobject. - Inline styles take a JS object with camelCased property names and are best reserved for values computed from props or state on each render.
- Conditional class names are usually built with a template literal and a ternary; always include a separating space between class names.
- React updates only the DOM attributes/properties that actually changed between renders, whether that’s a
classNamestring or individualstyleproperties.
