Styling approaches

React · Practical · 10 min

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, then import styles from "./Button.module.css" and use className={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

  • className takes a string; build conditional classes with clsx or a helper
  • CSS Modules scope class names per file; Tailwind keeps utility classes in JSX
  • Use style for 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.

Check yourself

  1. What does importing styles from './Card.module.css' give you that importing './Card.css' does not?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/learn#adding-styles

Builds on

Unlocks

Nothing builds on this yet.