Skip to content

CSS-in-JS: Runtime vs Zero-Runtime

advanced11 min read

The CSS-in-JS Spectrum

CSS-in-JS isn't one thing — it's a spectrum from fully runtime (styles computed and injected via JavaScript) to fully static (styles extracted to CSS files at build time). The trade-offs are dramatic: runtime solutions offer maximum dynamism but add JavaScript bundle size and block rendering. Zero-runtime solutions extract to static CSS but limit what you can do dynamically.

React Server Components pushed the conversation further — runtime CSS-in-JS doesn't work in server components because there's no JavaScript execution on the server to inject styles.

Mental Model

Think of the CSS-in-JS spectrum as a cooking analogy. Runtime CSS-in-JS (styled-components, Emotion) is like a chef cooking to order — every plate is custom, but service is slow during rush hour. Zero-runtime (vanilla-extract, Panda CSS) is like meal prep — dishes are prepared in advance and just need plating. The meals are the same quality, but prep-ahead serves faster at scale.

Runtime CSS-in-JS

How It Works

// styled-components example
import styled from 'styled-components';

const Button = styled.button`
  background: ${props => props.variant === 'primary' ? '#3b82f6' : 'transparent'};
  color: ${props => props.variant === 'primary' ? 'white' : '#3b82f6'};
  padding: 0.75rem 1.5rem;
  border: 2px solid #3b82f6;
  border-radius: 8px;
  cursor: pointer;

  &:hover {
    opacity: 0.9;
  }
`;

// Usage
<Button variant="primary">Click me</Button>

At runtime, styled-components:

  1. Parses the template literal
  2. Evaluates prop-based interpolations
  3. Generates a unique class name
  4. Injects a <style> tag into the document head
  5. Applies the class to the element

The Performance Cost

  • Bundle size: ~12-15KB gzipped for styled-components
  • Runtime: Style parsing and injection on every render (cached, but initial cost exists)
  • SSR complexity: Requires style extraction during server rendering to prevent flash of unstyled content
  • React Server Components: Incompatible — no JavaScript execution in server components

Emotion — Slightly Lighter

import { css } from '@emotion/react';

const buttonStyle = css`
  padding: 0.75rem 1.5rem;
  border-radius: 8px;
  background: var(--color-primary);
`;

// Or object syntax
const buttonObj = css({
  padding: '0.75rem 1.5rem',
  borderRadius: '8px',
  background: 'var(--color-primary)',
});

Zero-Runtime CSS-in-JS

vanilla-extract — Type-Safe Static CSS

// button.css.ts
import { style, styleVariants } from '@vanilla-extract/css';

const base = style({
  padding: '0.75rem 1.5rem',
  borderRadius: '8px',
  cursor: 'pointer',
  border: 'none',
  fontWeight: 500,
});

export const variants = styleVariants({
  primary: [base, { background: '#3b82f6', color: 'white' }],
  secondary: [base, { background: '#f3f4f6', color: '#1f2937' }],
  ghost: [base, { background: 'transparent', color: '#3b82f6' }],
});
// Button.tsx
import { variants } from './button.css';

export function Button({ variant = 'primary', children }) {
  return <button className={variants[variant]}>{children}</button>;
}

Key advantages:

  • TypeScript type checking for all CSS values
  • Extracts to static .css files at build time — zero JavaScript runtime
  • Works with React Server Components
  • Co-located with components but produces real CSS

Panda CSS — Zero-Runtime with Utility Patterns

import { css } from '../styled-system/css';

function Card() {
  return (
    <div className={css({
      padding: '6',
      borderRadius: 'lg',
      bg: 'white',
      shadow: 'md',
      _hover: { shadow: 'lg' },
    })}>
      Content
    </div>
  );
}

Panda CSS scans your files at build time, generates utility CSS, and outputs static stylesheets. It combines the DX of styled-components with the performance of static CSS.

Comparison Matrix

Featurestyled-componentsEmotionvanilla-extractPanda CSSCSS ModulesTailwind
Runtime JSYes (~15KB)Yes (~11KB)NoneNoneNoneNone
TypeScriptLimitedLimitedFullFullExternalExternal
Dynamic propsFullFullBuild-time onlyBuild-time onlyNoneNone
RSC compatibleNoNoYesYesYesYes
SSR setupComplexComplexNoneNoneNoneNone
ColocationExcellentExcellentGoodGoodGoodInline
Quiz
A Next.js app uses styled-components. After adding 'use client' to every component, styles work but performance degrades. Why?
Why runtime CSS-in-JS is declining

Three forces pushed the ecosystem toward zero-runtime:

  1. React Server Components: No JavaScript on the server means no runtime style injection. styled-components simply doesn't work in server components.

  2. Performance awareness: Google's Core Web Vitals penalize Total Blocking Time. Runtime CSS-in-JS adds JavaScript that blocks the main thread during style calculation and injection.

  3. CSS has caught up: CSS custom properties handle theming. :has(), container queries, and cascade layers solve problems that previously required JavaScript. The "escape from CSS" motivation is weaker now.

Common Trap

"Zero-runtime" doesn't mean zero build cost. vanilla-extract and Panda CSS both add complexity to your build pipeline. They need bundler plugins, can slow down builds on large codebases, and have their own learning curves. The zero-runtime label refers to browser runtime — the cost shifts to build time.

Execution Trace
Runtime (styled-components)
JS parses template literal → evaluates props → generates class → injects `<style>`
All happens in the browser at render time
Zero-runtime (vanilla-extract)
Build: .css.ts files → extract to .css files. Runtime: static className applied.
No JS style injection — just a class name string
RSC impact
Server Component renders on server with no JS runtime
Runtime CSS-in-JS can't inject styles. Zero-runtime class names work fine.
Hydration
Runtime CSS-in-JS must re-inject styles on client hydration
Zero-runtime styles already in the CSS file — no hydration mismatch risk
What developers doWhat they should do
Using styled-components in a Next.js App Router project with Server Components
Runtime CSS-in-JS requires client-side JavaScript, which doesn't exist in Server Components
Use vanilla-extract, Panda CSS, CSS Modules, or Tailwind for RSC-compatible styling
Choosing CSS-in-JS solely because it co-locates styles with components
Co-location is available in all modern approaches — it's not unique to CSS-in-JS
CSS Modules also co-locate styles. Evaluate based on runtime cost, type safety, and RSC compatibility.
Assuming zero-runtime means zero cost
The trade-off is browser performance for build complexity
Zero-runtime shifts cost to build time. Build pipeline complexity, compilation speed, and tooling overhead still exist.
Using runtime CSS-in-JS for styles that don't depend on props/state
Runtime CSS-in-JS is only justified when styles genuinely depend on runtime values that can't be handled by CSS custom properties
Static styles should always be static CSS (modules, Tailwind, vanilla-extract)
Quiz
Why doesn't styled-components work in React Server Components?
Quiz
Which CSS-in-JS solution provides TypeScript type checking for CSS property names and values?
Key Rules
  1. 1Runtime CSS-in-JS (styled-components, Emotion) adds JS bundle size and is incompatible with React Server Components
  2. 2Zero-runtime solutions (vanilla-extract, Panda CSS) extract to static CSS at build time — no browser JS cost
  3. 3CSS custom properties + CSS Modules handle most use cases that previously required CSS-in-JS
  4. 4Use runtime CSS-in-JS only when styles genuinely depend on runtime values that CSS variables can't express
  5. 5For new projects with Next.js App Router: prefer CSS Modules, Tailwind, vanilla-extract, or Panda CSS