Every project picks a styling strategy early; knowing the trade-offs avoids painful migrations.
React doesn't prescribe styling; it just gives you className and style. The main approaches:
- Plain CSS files imported into a module (
import "./App.css"). Vite bundles them. Simple, but class names are global, so collisions grow with the app. - CSS Modules: name the file
Button.module.css, thenimport styles from "./Button.module.css"and useclassName={styles.primary}. Class names are scoped to the file automatically. Zero runtime cost, supported by Vite and all frameworks out of the box. - Utility-first CSS such as Tailwind: compose small classes in JSX (
className="px-4 py-2 rounded bg-blue-600"). Very popular with React because styles live next to markup and unused CSS is stripped. - CSS-in-JS (styled-components, Emotion) defines styles in JavaScript. Runtime-based libraries add cost and don't fit well with Server Components; zero-runtime options (vanilla-extract, Panda, StyleX) compile to static CSS.
Whatever you choose, conditional classes are just strings. Build them with a template literal or a helper like clsx: className={clsx("tab", isActive && "tab-active")}.
The style prop takes an object with camelCase properties and is best for truly dynamic values (a computed width, a CSS variable): style={{ "--progress": pct + "%" }}. Prefer classes for everything static.
React 19 adds built-in support for <link rel="stylesheet" precedence="default"> anywhere in the tree: React hoists it to the head, de-duplicates it and waits for it to load before revealing the content that needs it. It also hoists <title> and <meta> rendered inside components into the document head.
Example
// Button.module.css
// .button { padding: 8px 16px; border-radius: 6px; }
// .primary { background: #0b63ce; color: white; }
import styles from "./Button.module.css";
function cx(...classes) {
return classes.filter(Boolean).join(" ");
}
export function Button({ primary = false, children, ...rest }) {
return (
<button className={cx(styles.button, primary && styles.primary)} {...rest}>
{children}
</button>
);
}
export function ProgressBar({ value }) {
// dynamic value via a CSS custom property
return <div className="progress" style={{ "--progress": `${value}%` }} />;
}
export default function Page() {
return (
<>
<title>Settings</title> {/* React 19 hoists this into <head> */}
<Button primary>Save</Button>
<ProgressBar value={42} />
</>
);
}Remember
classNametakes a string; build conditional classes withclsxor a helper- CSS Modules scope class names per file; Tailwind keeps utility classes in JSX
- Use
stylefor dynamic values; React 19 hoists<title>,<meta>and stylesheets
Watch out for
- Choosing a runtime CSS-in-JS library for a Server Components app, where it requires client-only workarounds.