Back to React
React
medium
mid

What are the different approaches to styling React components and when do you use each?

Six approaches: (1) plain CSS / SASS files; (2) CSS Modules — scoped class names per file; (3) Tailwind / Atomic CSS — utility classes; (4) CSS-in-JS (styled-components, emotion) — JS-authored styles with dynamic props; (5) zero-runtime CSS-in-JS (vanilla-extract, Linaria, Panda); (6) framework-native (Next.js global + module CSS). Modern default: Tailwind or CSS Modules. Avoid runtime CSS-in-JS for hot paths.

7 min read·~5 min to think through

Six approaches, each with tradeoffs.

1. Plain CSS / SASS

css
.button { background: blue; }
tsx
import './Button.css';
<button className="button">Click</button>

Pro: zero abstraction; everyone knows it. Con: global namespace — class collisions in big apps.

2. CSS Modules

css
/* Button.module.css */
.button { background: blue; }
tsx
import styles from './Button.module.css';
<button className={styles.button}>Click</button>

Pro: locally scoped class names; co-located with component; no runtime overhead. Con: no dynamic values from props (workaround: data attributes + CSS custom properties).

Default for many production apps.

3. Tailwind / Atomic CSS

tsx
<button className="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded">
  Click
</button>

Pro: no naming, no file switching, design-system tokens enforced, JIT compiler ships only used classes (~10 KB), excellent autocompletion via Tailwind IntelliSense. Con: JSX gets visually dense; cosmetic refactors mean rewriting long strings (clsx helps).

4. CSS-in-JS (styled-components, emotion)

tsx
import styled from 'styled-components';

const Button = styled.button`
  background: ${props => props.$primary ? 'blue' : 'gray'};
  padding: 8px 16px;
`;

<Button $primary>Click</Button>

Pro: dynamic styles via props, JS-authored. Con: runtime cost (style injection on render), SSR complications, bundle bloat, less mature in RSC.

The React team has publicly cooled on runtime CSS-in-JS for new projects.

5. Zero-runtime CSS-in-JS

vanilla-extract, Linaria, Panda, StyleX — write styles in TS/JS, compile to static CSS at build time.

ts
// styles.css.ts (vanilla-extract)
import { style } from '@vanilla-extract/css';

export const button = style({
  background: 'blue',
  padding: '8px 16px',
});
tsx
import * as styles from './styles.css';
<button className={styles.button}>Click</button>

Pro: typed styles, build-time extraction, no runtime cost. Con: build tooling, less ergonomic for one-offs than Tailwind.

6. Framework-native (Next.js)

Next.js supports CSS Modules, Tailwind, Sass, and global CSS out of the box. App Router adds CSS in RSC contexts without extra setup.

Recommendation matrix

ProjectBest
New SPA, design system to buildTailwind
Component library, distributableCSS Modules or Linaria (zero-runtime)
Heavy theming / dynamic stylesCSS variables + Tailwind, or vanilla-extract
Legacy migrationWhatever's already there, plus CSS Modules for new code
RSC-heavy appCSS Modules + Tailwind (runtime CSS-in-JS struggles)

Theming

Modern recommendation: CSS custom properties.

css
:root { --primary: #3b82f6; }
[data-theme='dark'] { --primary: #60a5fa; }
tsx
<button style={{ background: 'var(--primary)' }}>Click</button>

Switching theme is one DOM attribute change; no JS rerender required.

Class composition utilities

For Tailwind especially:

tsx
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

const cn = (...args) => twMerge(clsx(args));

<button className={cn('px-4 py-2', primary && 'bg-blue-500')}>...</button>

Senior framing

Styling choice is downstream of team and project. Tailwind dominates new green-field projects; CSS Modules is the conservative default; CSS-in-JS is fading for runtime reasons; zero-runtime alternatives are gaining. Pick once, document, enforce — fragmentation across the codebase is the real cost.

Follow-up questions

  • Why has the React team cooled on runtime CSS-in-JS?
  • When would you pick CSS Modules over Tailwind?
  • How do zero-runtime CSS-in-JS libraries work?

Common mistakes

  • Mixing four styling approaches in one codebase.
  • Picking CSS-in-JS in 2025 without checking RSC compatibility.
  • Hand-writing 200-class Tailwind strings without a clsx/cn helper.

Performance considerations

  • Runtime CSS-in-JS: style injection cost on every render. Tailwind: zero runtime cost; JIT outputs ~10 KB. CSS Modules: zero runtime cost. Zero-runtime libraries: compile-time cost only. Bundle and runtime both matter; profile both.

Edge cases

  • CSS-in-JS + RSC: most runtime libraries don't work in server components.
  • Tailwind + dynamic classes: must include in safelist or use clsx with full string.
  • CSS variables don't cascade through shadow DOM.

Real-world examples

  • Vercel (Tailwind), GitHub (Primer CSS), Linear (custom CSS Modules), Stripe (proprietary). New projects in 2024+ lean Tailwind or vanilla-extract.

Senior engineer discussion

Senior framing: styling debates are religious. The technical answer is: pick zero-runtime (Tailwind, CSS Modules, vanilla-extract) for production, build a clean design system, enforce via lint. The exact choice matters less than the consistency.