Concept
CSS-in-JS writes styles directly in JavaScript/TypeScript files, colocated with the component that uses them, with the ability to compute style values genuinely dynamically from JS, component props, state, theme context, directly inline, without an intermediate class-toggling or CSS custom property step. This is the actual differentiating capability versus CSS Modules/Tailwind, and it comes with a real, historically significant runtime cost for traditional implementations that's the central trade-off to understand.
Basic usage, styled-components
import styled from "styled-components";
const Button = styled.button`
padding: 8px 16px;
border-radius: 6px;
background: ${(props) => (props.variant === "primary" ? "#4f46e5" : "#e5e7eb")};
color: ${(props) => (props.variant === "primary" ? "white" : "#111827")};
`;
<Button variant="primary">Submit</Button>;Basic usage, Emotion (both styled API and css prop)
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
function Button({ variant }) {
return (
<button
css={css`
padding: 8px 16px;
background: ${variant === "primary" ? "#4f46e5" : "#e5e7eb"};
`}
>
Submit
</button>
);
}Both libraries let a style block reference props/component-scope JS values directly inside the CSS itself, genuinely computed styling, not a lookup between a fixed set of pre-defined class name variants. This is the real capability CSS Modules and Tailwind don't offer as naturally: a progress bar whose width is a literal numeric prop value, a color interpolated from a theme object at arbitrary granularity, styles computed from complex conditional logic spanning multiple props at once.
The runtime cost, why this became a genuinely contentious trade-off
Traditional CSS-in-JS libraries (styled-components, Emotion, in their default/classic mode) do real work in the browser, at render time: parsing the tagged template literal, generating a unique class name, injecting the actual CSS rule into a <style> tag in the document, and re-doing relevant parts of this whenever props change and the computed style differs. This is fundamentally different from CSS Modules or Tailwind, both of which resolve everything at build time, by the time the browser receives the page, the CSS already exists as static rules, with zero JS-driven style generation happening at runtime.
The practical impact: measurable JS execution cost on initial render and on prop changes that affect styling, larger JS bundle size (shipping the CSS-in-JS runtime library itself to the browser), and, specifically relevant to React Server Components/Next.js App Router, traditional CSS-in-JS libraries historically had real friction with server rendering, since injecting styles into a <style> tag via a client-side JS runtime doesn't naturally fit a server-rendering-first model without extra integration work.
Zero-runtime CSS-in-JS, the industry's response to this trade-off
// vanilla-extract example, TypeScript file, but compiles entirely to static CSS at BUILD time
import { style } from "@vanilla-extract/css";
export const button = style({
padding: "8px 16px",
background: "#4f46e5",
});Libraries like vanilla-extract, Linaria, and Emotion/styled-components' own newer compiler-based modes extract styles at build time wherever the values are statically determinable, shipping plain static CSS to the browser and eliminating the runtime injection cost, while still offering (a constrained form of) the colocated, TypeScript-checked authoring experience that made CSS-in-JS appealing in the first place. This is widely understood as where the ecosystem has been converging: genuinely dynamic, per-render prop-driven values still require some runtime mechanism (typically CSS custom properties set dynamically, rather than full style recomputation), but static styles compile away entirely, capturing most of the performance benefit of CSS Modules/Tailwind while keeping more of CSS-in-JS's authoring ergonomics.
Server Components and the current friction point
Traditional CSS-in-JS's runtime style injection model is a genuine architectural mismatch with React Server Components, a Server Component has no client-side JS runtime to inject a <style> tag with at all, since it doesn't hydrate on the client. This is a concrete, current reason several teams building on Next.js App Router specifically have moved toward CSS Modules, Tailwind, or zero-runtime CSS-in-JS rather than traditional styled-components/Emotion, not a stylistic preference, but a genuine architectural compatibility issue with the newer rendering model.
Common Mistakes
1. Using dynamic prop-driven styles for values that are actually static/limited-variant
// Overkill, this is really just 2-3 fixed variants, better expressed as CSS Modules classes or Tailwind variant classes
const Button = styled.button`
background: ${(props) => (props.variant === "primary" ? "#4f46e5" : props.variant === "danger" ? "#dc2626" : "#e5e7eb")};
`;If the actual set of style variations is small and fixed (a handful of button variants), that's arguably better expressed as static classes (CSS Modules, or Tailwind conditional class selection) rather than paying CSS-in-JS's runtime cost for what's fundamentally not dynamic, continuously-variable styling, reserve genuine prop-interpolated CSS-in-JS for cases where the value truly is computed/continuous (an arbitrary numeric width, an interpolated color).
2. Not measuring actual runtime performance impact before assuming it's negligible
Traditional CSS-in-JS's runtime cost is real but scales with usage, a small app with modest component counts may never notice it, while a large app with thousands of styled-components instances mounting/updating can see it clearly in profiling. Don't assume "modern CSS-in-JS is fast enough" without actually profiling the specific application's real usage pattern.
3. Using traditional (non-zero-runtime) CSS-in-JS inside React Server Components without understanding the incompatibility
Attempting to use styled-components/Emotion's classic runtime injection model directly inside a Server Component (no client JS context to inject a style tag into) either doesn't work as expected or requires falling back to marking the component as a Client Component, defeating some of the purpose of using Server Components for that piece in the first place.
4. Choosing a CSS-in-JS library based on age/popularity without checking its current zero-runtime story
The CSS-in-JS landscape has shifted meaningfully in recent years, a recommendation or codebase pattern from several years ago may predate the zero-runtime alternatives that meaningfully change the trade-off calculus; evaluate current options rather than defaulting to whichever library was most popular when a team's habits were formed.
5. Assuming CSS-in-JS solves specificity/architecture problems automatically
Like CSS Modules, CSS-in-JS solves naming/scoping collisions, it doesn't automatically produce good component architecture or prevent overly complex, hard-to-reason-about conditional styling logic sprawling across many props. The same discipline about keeping styling logic legible still applies.
Best Practices
- Reserve genuinely dynamic prop-driven CSS-in-JS for values that are actually continuous/computed, not a small fixed set of variants better expressed as static classes.
- Evaluate zero-runtime options (vanilla-extract, Linaria, or compiler-mode Emotion/styled-components) for new projects, given the meaningfully different performance profile versus classic runtime CSS-in-JS.
- Profile actual runtime cost in a representative application before assuming it's negligible, don't rely on intuition alone for a performance-sensitive decision.
- Understand the React Server Components compatibility question before committing to a traditional CSS-in-JS library on a Next.js App Router project specifically.
- Colocate styles with components (CSS-in-JS's genuine ergonomic strength) without necessarily accepting full runtime style injection, zero-runtime approaches increasingly offer both.
Further Resources
- styled-components, official docs
- Emotion, official docs
- vanilla-extract, zero-runtime CSS-in-JS
- Linaria, zero-runtime CSS-in-JS
- Sam Magura, The State of CSS-in-JS (ongoing industry discussion) (community-maintained comparison resource)
- Josh W. Comeau, Why I Don't Use CSS-in-JS, a well-argued critical perspective, useful for understanding the trade-offs from the other side.
