CSS-in-JS vs CSS Modules

Difficulty: Intermediate

Question

What are CSS Modules and CSS-in-JS? What are the trade-offs?

Answer

Both solve CSS scoping (preventing style conflicts) in different ways.

CSS Modules: Write regular CSS, build tool generates unique class names. - Scoped by default, no conflicts - Zero runtime cost (compiled at build time) - Regular CSS syntax, easy to learn - Works with any framework

CSS-in-JS (styled-components, Emotion): Write CSS in JavaScript. - Dynamic styles based on props/state - Colocated with component code - Runtime cost (generates CSS at runtime) - Framework-specific (usually React)

Modern trend: Moving away from runtime CSS-in-JS toward zero-runtime solutions (CSS Modules, Tailwind, vanilla-extract, Panda CSS).

Code examples

CSS Modules

/* Button.module.css */
.button {
  padding: 0.75rem 1.5rem;
  border-radius: 0.375rem;
  font-weight: 500;
}
.primary {
  background: #3b82f6;
  color: white;
}
.danger {
  background: #ef4444;
  color: white;
}

/* Button.tsx */
import styles from './Button.module.css';

function Button({ variant = 'primary', children }) {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  );
}
// Rendered: <button class="Button_button_x3k2 Button_primary_a1b2">
// Unique class names = no conflicts

CSS Modules add a unique hash to class names at build time. Zero runtime cost. Regular CSS syntax. Works with Vite, Webpack, etc.

CSS-in-JS (styled-components)

import styled from 'styled-components';

// Define styled component
const Button = styled.button`
  padding: 0.75rem 1.5rem;
  border-radius: 0.375rem;
  font-weight: 500;
  background: ${props => props.variant === 'danger'
    ? '#ef4444' : '#3b82f6'};
  color: white;
  opacity: ${props => props.disabled ? 0.5 : 1};

  &:hover {
    filter: brightness(1.1);
  }
`;

// Usage
function App() {
  return (
    <>
      <Button>Primary</Button>
      <Button variant="danger">Delete</Button>
      <Button disabled>Disabled</Button>
    </>
  );
}
// Styles are injected into <head> at runtime

styled-components generates unique classes at runtime. Pros: dynamic styles with props, colocated code. Cons: runtime performance cost, larger bundle.

Comparison and Modern Alternatives

/* 1. Plain CSS / BEM */
.card { }           /* Global, manual scoping */

/* 2. CSS Modules */
.card { }           /* Auto-scoped, build-time */
/* import s from './Card.module.css' */

/* 3. Tailwind (utility-first) */
/* <div class="p-6 bg-white rounded-lg shadow"> */
/* Zero-runtime, atomic classes */

/* 4. styled-components / Emotion (runtime) */
/* const Card = styled.div`padding: 1.5rem` */
/* Runtime CSS injection */

/* 5. vanilla-extract (zero-runtime CSS-in-TS) */
/* export const card = style({ padding: '1.5rem' }) */
/* Type-safe, build-time extraction */

/* Trade-offs:
   Runtime CSS-in-JS: dynamic + colocation - performance
   CSS Modules: simple + performant - no dynamic props
   Tailwind: fast + consistent - verbose HTML
   vanilla-extract: type-safe + zero-runtime - setup
*/

The industry is moving toward zero-runtime solutions. Tailwind is currently the most popular. CSS Modules remain excellent for traditional CSS workflows.

Key points

Concepts covered

CSS-in-JS, CSS Modules, Styled Components, Scoping, Build-time vs Runtime