Skip to content
fearchitect
Styling & Design Systems

CSS Modules vs CSS-in-JS vs Tailwind

CSS Modules, runtime vs zero-runtime CSS-in-JS, and utility-first — by runtime cost and RSC fit.

By Abas TurabliReviewed

Summary

CSS Modules, runtime CSS-in-JS (styled-components/Emotion), zero-runtime CSS-in-JS (vanilla-extract, StyleX, Linaria), and utility-first (Tailwind) sit on a spectrum from static to dynamic. Runtime CSS-in-JS injects styles from JavaScript — fast to author but incompatible with React Server Components. Zero-runtime and Modules compile to static files, keeping the server boundary clean.

Jump to the interview angle

CSS strategy spectrum

A range of styling approaches that differ in when styles are generated: at build time (CSS Modules, zero-runtime CSS-in-JS, utility-first) or at runtime in the browser (runtime CSS-in-JS). Build-time approaches produce static CSS files; runtime approaches produce styles by executing JavaScript on the client, which blocks RSC adoption because Server Components run on the server with no browser JS context.

Approach × runtime cost / RSC-compat / DX

ApproachRuntime JS costRSC compatibleDX
CSS ModulesNone — static .css filesYes — plain static importScoped classes; no colocated variants
Runtime CSS-in-JS (styled-components, Emotion)High — style insertion runs per renderNo — requires client JS contextProps-driven variants; full TS colocation
Zero-runtime CSS-in-JS (vanilla-extract, StyleX, Linaria)None — compiled to static CSS at buildYes — outputs .css filesType-safe; colocated; no dynamic props at runtime
Utility-first (Tailwind CSS)None — static utility classes; JIT purges unusedYes — class strings onlyFast iteration; verbose JSX; design-token constraints

Why runtime CSS-in-JS breaks RSC

styled-components and Emotion insert styles by calling document.createElement('style') at render time. React Server Components execute on the server (or at build time) with no DOM — that call throws. Wrapping every styled component in 'use client' is possible but defeats RSC's bundle-splitting goal. Teams migrating to the App Router switched to vanilla-extract or Tailwind.

How to choose

Greenfield RSC app: Tailwind (fast, zero overhead) or vanilla-extract (type-safe, colocated). Legacy SPA staying on client-only React: runtime CSS-in-JS is fine. Design-system package shared across RSC and non-RSC consumers: zero-runtime (vanilla-extract or StyleX). Thin component library with no styling opinions: CSS Modules.

Interview angle

Interviewers ask why teams moved off styled-components when adopting the Next.js App Router, or how you would style a shared design-system that ships to RSC consumers. Name the DOM-injection mechanism and the RSC constraint specifically.

Soundbite: "Runtime CSS-in-JS writes styles via JavaScript — that's incompatible with a server that has no DOM."

Key terms

Runtime CSS-in-JS
Styles generated and injected into the DOM by JavaScript executing in the browser.
Zero-runtime CSS-in-JS
CSS-in-JS tooling (vanilla-extract, StyleX, Linaria) that compiles styles to static files at build time.
CSS Modules
Locally-scoped CSS via build-time class-name hashing; no JS at runtime.
Utility-first CSS
Tailwind's approach: a fixed set of single-purpose classes; unused ones are purged at build.
JIT (Tailwind)
Just-in-time compiler that scans source files and emits only the utility classes actually used.

Further reading

Search fearchitect

Jump to a topic, mode, or action.