Difficulty: Intermediate
What are CSS Modules and CSS-in-JS? What are the trade-offs?
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).
/* 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.
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.
/* 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.
CSS-in-JS, CSS Modules, Styled Components, Scoping, Build-time vs Runtime