Topic #08Core17 min read

CSS-in-JS / CSS Modules (Emotion)

How CSS Modules scope class names at build time with zero runtime cost, and how Emotion generates scoped, prop-driven styles at runtime — the styled and css APIs, theming, SSR extraction, the flash-of-unstyled-content trap, zero-runtime alternatives, and when to pick each.

#css-modules#emotion#css-in-js#scoping#react#ssr#theming#zero-runtime

Two tools, one shared job. Both CSS Modules and Emotion solve CSS's global-scope problem, but at opposite ends of the build-vs-runtime axis. CSS Modules rename your class names uniquely at build time and leave you with plain, static CSS — zero runtime cost. Emotion generates scoped class names at runtime (or build time in its extracted modes) and lets styles be computed from props and theme. Many real codebases use both: Modules for the static bulk, Emotion for the genuinely dynamic pieces.

CSS Modules — how the scoping works. You author a normal stylesheet named Something.module.css and import styles from './Something.module.css'. The bundler rewrites every local class name to a globally-unique mangled name (.card becomes something like ExpenseCard_card_a1b2c) and hands you back a JS object mapping the original name to the mangled one. You reference styles.card in className, so two components can each define .card with no collision. It's still just CSS — the same performance, the same tooling — only automatically namespaced per file.

CSS Modules — composition and globals. Modules add a composes keyword to inherit declarations from another local (or imported) class, which is a lightweight alternative to preprocessor mixins. When you genuinely need an unscoped selector — a third-party class, a body reset — you opt out with :global(.selector). Everything else stays local by default, which is the point: local-first scoping with an explicit escape hatch, rather than global-first with manual discipline.

Emotion — the two APIs. Emotion offers two main ways to write styles. The styled API creates a component from a tag with attached styles: const Card = styled.div\...` — the styles can interpolate functions of props (${p => p.active ? 'blue' : 'grey'}), so the same component renders differently based on state. The **cssprop API** attaches a computed style object or template directly to any element:<p css={dynamicStyle(hasError)}>— no wrapper component needed.styledsuits reusable design-system primitives; thecss` prop suits one-off, local styling.

Emotion — how it generates and injects styles. At runtime Emotion serializes your style into a string, hashes it to produce a stable, unique class name (css-1a2b3c), and injects the corresponding rule into a <style> tag in the document head, deduping identical styles across components. Because the class name is derived from the content of the styles, two elements with identical styles share one class — a nice automatic dedupe — while prop-driven variations produce distinct classes as needed.

Theming and design tokens. Emotion ships a ThemeProvider that puts a theme object into React context; styled/css functions then receive theme as an argument (\${p => p.theme.colors.brand}). This centralises tokens and enables runtime theme switching (light/dark, white-label brands) by swapping the provided object. It's the CSS-in-JS answer to the same design-token layer Tailwind keeps in config — colocated with components and reactive to context.

The runtime cost, honestly. Classic CSS-in-JS pays three costs CSS Modules don't: a styling engine in the JS bundle, style serialization and injection during render (work on every relevant render), and the need to extract critical CSS on the server. On heavy, frequently-rerendering trees (large virtualized lists) this shows up in profiles. It's not disqualifying for most apps, but it's the reason performance-sensitive teams reach for CSS Modules or zero-runtime tools instead.

Server-side rendering and the FOUC trap. On the server, Emotion must collect the styles used while rendering and inline them into the HTML <head>; frameworks like Next.js need explicit integration for this. Get it wrong and the browser shows a flash of unstyled content (FOUC) before the client injects styles, plus possible hydration mismatches if server and client class names diverge. CSS Modules sidestep this entirely because their output is a static .css file the browser loads normally — no runtime injection, no FOUC.

Zero-runtime CSS-in-JS. To keep the ergonomics (colocation, styled syntax, theming) but drop the runtime cost, tools like Linaria and vanilla-extract move the extraction to build time: you write CSS-in-JS, and they emit static .css plus class-name constants, with dynamic bits handled via CSS custom properties. This is increasingly the recommended path in performance-conscious and RSC-heavy stacks, and it's why many teams that once used Styled Components have migrated.

How to choose between them. Use CSS Modules for the default, static bulk of an app: zero runtime cost, standards-based, trivial SSR, great tooling. Reach for Emotion (or another runtime CSS-in-JS) when styling is genuinely prop-driven and tightly coupled to complex component state, or when you want a themed design-system with runtime theme switching. When performance matters but you like the CSS-in-JS authoring model, prefer a zero-runtime tool. And remember CSS custom properties can give Modules most of the 'dynamic' behavior without any runtime library.

Common gotchas. (1) Emotion without SSR setup causes FOUC and hydration mismatches — wire up the server integration. (2) Defining styled components inside a render function recreates them every render, breaking memoization and hurting performance — define them at module scope. (3) Interpolating props into styles that change often generates many unique classes; prefer CSS variables for high-frequency values. (4) CSS Modules class names are mangled, so you can't target them from external global CSS — use :global deliberately. (5) Overusing the css prop for everything can scatter styles and hurt readability; reserve styled for reusable primitives.

The mental model (memorise this). CSS Modules = build-time renaming to unique class names, static CSS, zero runtime, trivial SSR — your default for static styles. Emotion = runtime (or build-time-extracted) scoped classes computed from props and theme — reach for it when styling must react to state, accept the runtime and SSR cost, define styled components at module scope, and lean on CSS variables for hot-path dynamic values.

Backend Analogy

CSS Modules are like Java packages plus the compiler: at build time the class name `card` gets fully-qualified into a unique symbol (`ExpenseCard_card_a1b2c`) so two modules can both declare `card` with zero collision — namespacing resolved once, statically, no runtime lookup. Emotion is like building configuration at runtime from injected parameters: a factory bean that reads request-scoped state and produces a tailored result each call — maximally flexible (styles as a function of props and a themed context, exactly like a bean assembled from a ThemeProvider) but you pay serialization/injection cost on the hot path, and you must arrange for it to work correctly during server rendering (SSR extraction is like pre-warming a cache so the first response is fully populated instead of blank). Zero-runtime CSS-in-JS (Linaria/vanilla-extract) is annotation processing / build-time code generation: you write the expressive high-level form, and the toolchain emits the static, cheap artifact ahead of time so nothing extra runs in production.

Key Insights
  • CSS Modules and Emotion both solve CSS's global-scope problem but sit at opposite ends of the build-vs-runtime axis.
  • CSS Modules rename class names uniquely at build time and yield static CSS with zero runtime cost; importing a .module.css gives you an object of mangled, collision-proof names.
  • CSS Modules are local-scoped by default with explicit escape hatches: :global for unscoped selectors and composes to inherit declarations from another class.
  • Emotion's styled API creates prop-aware components; the css prop attaches a computed style directly to an element — styled for reusable primitives, css prop for one-offs.
  • Emotion hashes each unique style into a stable class name and injects it into a style tag at runtime, deduping identical styles automatically.
  • Emotion's ThemeProvider puts tokens in context so styles can read theme and switch themes at runtime — the CSS-in-JS design-token layer.
  • Classic CSS-in-JS costs a runtime engine, per-render style injection, and SSR extraction; CSS Modules avoid all three because their output is a static stylesheet.
  • Without SSR setup Emotion causes a flash of unstyled content and hydration mismatches; CSS Modules sidestep this entirely.
  • Zero-runtime tools like Linaria and vanilla-extract keep the CSS-in-JS authoring model but extract static CSS at build time, reclaiming the performance.
  • Never define styled components inside a render function — it recreates them every render; define at module scope, and use CSS variables for high-frequency dynamic values.

Worked Code

CSS Modules: scoped classes, composes, and :global
CSS
/* ExpenseCard.module.css — authored as normal CSS, scoped at build time */
.base {
  padding: 16px;
  border-radius: 8px;
  border: 1px solid #ddd;
}
.card {
  composes: base;              /* inherit declarations from .base (like a mixin) */
  transition: box-shadow .2s;
}
.card:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Opt OUT of scoping for a genuinely global selector (use deliberately) */
:global(.no-print) { display: none; }
/* At build time .card becomes e.g. ExpenseCard_card_a1b2c — unique per file */
CSS Modules: consuming the mangled class map
TSX
// The import is a JS object mapping local names -> unique mangled names.
import styles from "./ExpenseCard.module.css";

function ExpenseCard({ title }: { title: string }) {
  // styles.card === "ExpenseCard_card_a1b2c" (scoped, collision-proof)
  return <div className={styles.card}>{title}</div>;
}

// Combining classes conditionally: build the string yourself or use clsx.
import clsx from "clsx";
function Row({ active }: { active: boolean }) {
  return <div className={clsx(styles.card, active && styles.active)} />;
}
Emotion: prop-driven styles with the styled API
TSX
// Define styled components at MODULE scope — never inside render.
import styled from "@emotion/styled";

// Styles are a function of props: the component renders differently by state.
const Card = styled.div<{ active?: boolean }>`
  padding: 16px;
  border-radius: 8px;
  border: 1px solid ${(p) => (p.active ? "#1B4F72" : "#ddd")};
  background: ${(p) => (p.active ? "#EBF5FB" : "white")};
  transition: all 0.2s;
  &:hover {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }
`;

// Usage — the appearance is a pure function of the active prop.
function Item({ selected }: { selected: boolean }) {
  return <Card active={selected}>Lunch — Rs.120</Card>;
}
Emotion: the css prop + ThemeProvider tokens
TSX
/** @jsxImportSource @emotion/react */
import { css, ThemeProvider } from "@emotion/react";

const theme = { colors: { ok: "green", error: "red", brand: "#1B4F72" } };

// A computed style factory — one-off styling without a wrapper component.
const status = (isError: boolean) =>
  css`
    color: ${isError ? "red" : "green"};
    font-weight: bold;
  `;

function App({ hasError }: { hasError: boolean }) {
  return (
    <ThemeProvider theme={theme}>
      {/* css prop reads the computed style; styled/css fns also receive theme */}
      <p css={status(hasError)}>Status message</p>
    </ThemeProvider>
  );
}
Hot-path dynamic values: prefer CSS variables over prop interpolation
TSX
import styled from "@emotion/styled";

// ANTI-PATTERN: interpolating a frequently-changing value generates a new
// hashed class on every distinct value — many classes, more injection work.
const BadBar = styled.div<{ pct: number }>`
  width: ${(p) => p.pct}%;
`;

// BETTER: keep ONE static class; pass the changing value via a CSS variable.
const Bar = styled.div`
  width: var(--pct);
  height: 8px;
  background: #1B4F72;
`;
function Progress({ percent }: { percent: number }) {
  return <Bar style={{ ["--pct" as string]: percent + "%" }} />;
}

Try It Live

Edit the code and press Run — it executes safely in a sandboxed iframe. Use the Console tab for log output.

What Emotion produces: the plain-CSS output of scoped, prop-driven styles

Interview-Ready Q&A

They sit at opposite ends of the build-vs-runtime axis. CSS Modules rename class names uniquely at build time and produce static, plain CSS with zero runtime cost — you author real CSS and import a map of mangled names. Emotion writes CSS in JavaScript and generates scoped class names at runtime, which enables prop-driven dynamic styles and theming, at the cost of a runtime engine, per-render injection, and SSR complexity.

Things to Remember
  • 1CSS Modules: build-time scoping, static CSS, zero runtime cost, mangled class names, trivial SSR.
  • 2Modules are local by default; use :global to opt out and composes to inherit declarations.
  • 3Emotion styled = prop-aware reusable components; css prop = one-off styling on an element.
  • 4Emotion hashes each unique style into a class and injects it at runtime, deduping identical styles.
  • 5ThemeProvider puts tokens in context, enabling runtime theme switching.
  • 6CSS-in-JS costs: runtime engine, per-render injection, and SSR extraction — Modules avoid all three.
  • 7No SSR setup => flash of unstyled content and hydration mismatches; wire up server integration.
  • 8Define styled components at module scope, never inside render.
  • 9Use CSS variables for high-frequency dynamic values to avoid class explosion.
  • 10For performance with CSS-in-JS ergonomics, prefer zero-runtime tools (Linaria, vanilla-extract).

References & Further Reading