CSS-in-JS peaked in popularity when component-scoped styling and dynamic theming were hard problems to solve any other way — since then, CSS Modules, native CSS nesting, and utility-first frameworks like Tailwind have each chipped away at the specific pain points CSS-in-JS was built to solve, which is worth understanding before reaching for it by default.
CSS-in-JS is a pattern where component styles are written in JavaScript/TypeScript, colocated with the component itself, rather than in separate .css files. Libraries implementing this pattern (styled-components, Emotion, vanilla-extract) vary significantly in whether styles are computed at runtime (in the browser) or extracted at build time — a distinction with real performance implications.
Why CSS-in-JS Matters (and When to Skip It)
Colocating styles with components and having full access to JavaScript logic for dynamic styling (props-based conditional styles, computed values) is genuinely convenient for component libraries with significant prop-driven visual variation — the styling logic lives next to the component logic it's tied to, rather than in a separate stylesheet you have to cross-reference mentally.
Skip runtime CSS-in-JS specifically for performance-sensitive applications, since runtime style computation and injection has a real cost (JavaScript execution generating styles, injecting them into the DOM, and this cost happens on every render in some implementations) that build-time alternatives (CSS Modules, vanilla-extract, Tailwind) avoid entirely by resolving styles ahead of time.
Getting Started with CSS-in-JS
A styled-components example, showing the colocated, dynamic-styling pattern:
import styled from "styled-components";
const Button = styled.button<{ variant: "primary" | "secondary" }>`
padding: 0.5rem 1rem;
background-color: ${(props) => (props.variant === "primary" ? "#3b82f6" : "gray")};
color: white;
&:hover {
opacity: 0.9;
}
`;
function App() {
return <Button variant="primary">Click me</Button>;
}
A build-time alternative (vanilla-extract), avoiding runtime style computation:
// button.css.ts
import { style } from "@vanilla-extract/css";
export const button = style({
padding: "0.5rem 1rem",
backgroundColor: "#3b82f6",
":hover": { opacity: 0.9 },
});
Core CSS-in-JS Concepts Every Developer Should Know
Runtime CSS-in-JS computes and injects styles in the browser, which means a genuine performance cost (particularly with server-side rendering, where styles need to be extracted and injected during SSR to avoid a flash of unstyled content) — this runtime cost is the main reason the CSS-in-JS category as a whole has faced more scrutiny in recent years as performance-conscious defaults have shifted.
Build-time (zero-runtime) CSS-in-JS libraries generate static CSS files at build time, keeping the colocation and type-safety developer experience benefits of CSS-in-JS while avoiding the runtime cost — vanilla-extract and similar tools represent this middle ground, and are generally the better choice if you specifically want CSS-in-JS's DX without its historical performance downsides.
React Server Components complicate runtime CSS-in-JS significantly, since many runtime CSS-in-JS libraries rely on React context and client-side JavaScript execution patterns that don't map cleanly onto the server-component model — this is a meaningful, concrete reason several teams have moved away from runtime CSS-in-JS specifically in Next.js App Router projects.
Dynamic, prop-driven styles are CSS-in-JS's clearest remaining advantage over CSS Modules or Tailwind — when a component's styling genuinely depends on complex runtime logic beyond what CSS custom properties comfortably express, CSS-in-JS's direct JavaScript access is a real ergonomic benefit, though this is a narrower use case than CSS-in-JS's original broad popularity suggested.
Common CSS-in-JS Mistakes and How to Fix Them
Mistake 1: reaching for runtime CSS-in-JS by default without considering its performance cost or React Server Components compatibility, particularly in a Next.js App Router project. Fix: evaluate build-time alternatives (vanilla-extract, CSS Modules, Tailwind) first, and use runtime CSS-in-JS only where its specific dynamic-styling benefit is genuinely needed.
Mistake 2: not properly configuring SSR style extraction, causing a flash of unstyled content on initial page load. Fix: follow your CSS-in-JS library's specific SSR integration guide carefully, since misconfiguration here is a common and visible bug.
Mistake 3: using CSS-in-JS for styles that don't actually need dynamic, prop-driven logic, when a static CSS Module or utility classes would be simpler and avoid the runtime cost entirely. Fix: reserve CSS-in-JS's dynamic capabilities for cases that actually need them, using simpler static styling for everything else.
When Should You Use CSS-in-JS Instead of CSS Modules or Tailwind?
Use CSS-in-JS (preferably a build-time/zero-runtime option) when component styling genuinely depends on complex, prop-driven logic that's awkward to express with CSS custom properties or conditional class names. Use CSS Modules or Tailwind for most other styling needs, where their build-time resolution and simpler mental model avoid CSS-in-JS's historical performance and React Server Components compatibility concerns.
CSS-in-JS in Production
If already using runtime CSS-in-JS in a Next.js App Router project, verify actual React Server Components compatibility carefully, since this is an active pain point for several popular runtime libraries. Consider migrating toward a build-time/zero-runtime alternative (vanilla-extract) for new work if colocation and type safety matter but the runtime cost doesn't.
If you're starting a new project and reaching for CSS-in-JS out of habit, evaluate Tailwind or a build-time option first — the specific problems runtime CSS-in-JS was built to solve are now handled well by tools without its performance and SSR complexity tradeoffs.