CSS-in-JS Concepts

Difficulty: Advanced

CSS-in-JS is an approach where CSS styles are written directly in JavaScript files, co-located with the components they style. Instead of separate .css files, styles are defined as JavaScript objects or tagged template literals, and the library handles generating unique class names and injecting the CSS into the document. This approach gained popularity with React because it solves scoping, dead code elimination, and dynamic styling challenges that are difficult with traditional CSS.

The most well-known CSS-in-JS library is styled-components, which uses tagged template literals to create React components with embedded styles. When you write const Button = styled.button`background: blue;`, styled-components creates a React component that renders a <button> with a generated unique class name and the corresponding CSS injected into a <style> tag. The key innovation is that these styles can reference JavaScript variables and component props, enabling truly dynamic styling that would require inline styles or complex class toggling in traditional CSS.

Runtime CSS-in-JS libraries (styled-components, Emotion, JSS) generate and inject CSS at runtime in the browser. When a component renders, the library serializes the styles, generates a hash-based class name, and inserts a <style> tag into the document head. This enables powerful features like prop-based dynamic styles, theming via React context, automatic vendor prefixing, and automatic critical CSS extraction. However, the runtime cost is the main tradeoff: serializing styles, computing hashes, and injecting CSS adds overhead to every render, which can impact performance in large applications.

Zero-runtime CSS-in-JS libraries (Linaria, vanilla-extract, Panda CSS, StyleX) extract styles at build time and produce static CSS files with zero JavaScript runtime. You write styles using a similar API, but a compiler processes them during the build step, replacing the style definitions with generated class names and outputting standard CSS files. This gives you the developer experience of CSS-in-JS (co-location, type safety, variables) without any runtime performance cost. The tradeoff is that dynamic styles based on runtime values require CSS custom properties or predefined variants rather than arbitrary expressions.

The CSS-in-JS landscape has evolved significantly. The original premise -- that co-locating styles with components improves maintainability -- remains valid. However, the industry trend has shifted away from runtime solutions toward zero-runtime alternatives and CSS Modules. The React team's introduction of Server Components made runtime CSS-in-JS libraries incompatible with streaming SSR, accelerating this shift. Today, the recommended approaches for new React projects are CSS Modules, Tailwind CSS, or zero-runtime CSS-in-JS. Runtime libraries like styled-components remain widely used in existing codebases and are not going away, but new projects increasingly choose alternatives with lower performance overhead.

Code examples

Styled-components: Basic usage and props

import styled from 'styled-components';

// Basic styled component
const Card = styled.div`
  background: #fff;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
`;

// Dynamic styles based on props
const Button = styled.button`
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: opacity 0.2s;

  /* Dynamic background based on variant prop */
  background: ${(props) => {
    switch (props.variant) {
      case 'primary': return '#3b82f6';
      case 'danger': return '#ef4444';
      default: return '#e5e7eb';
    }
  }};

  color: ${(props) =>
    props.variant === 'primary' || props.variant === 'danger'
      ? '#fff'
      : '#374151'
  };

  opacity: ${(props) => (props.disabled ? 0.5 : 1)};

  &:hover {
    opacity: 0.9;
  }
`;

// Extending styles
const OutlineButton = styled(Button)`
  background: transparent;
  border: 2px solid #3b82f6;
  color: #3b82f6;
`;

// Usage
function App() {
  return (
    <Card>
      <h2>Styled Components</h2>
      <Button variant="primary">Save</Button>
      <Button variant="danger">Delete</Button>
      <OutlineButton>Cancel</OutlineButton>
    </Card>
  );
}

Styled-components uses tagged template literals to create React components with embedded styles. Props are available inside the template via interpolation functions. The styled() function extends existing styled components. Each component gets a unique class name automatically.

Theming with CSS-in-JS

import styled, { ThemeProvider, css } from 'styled-components';

// Define theme objects
const lightTheme = {
  colors: {
    bg: '#ffffff',
    text: '#111827',
    primary: '#3b82f6',
    border: '#e5e7eb',
    muted: '#6b7280',
  },
  spacing: {
    sm: '8px',
    md: '16px',
    lg: '24px',
  },
  radii: {
    sm: '4px',
    md: '8px',
    lg: '12px',
  },
};

const darkTheme = {
  colors: {
    bg: '#111827',
    text: '#f9fafb',
    primary: '#60a5fa',
    border: '#374151',
    muted: '#9ca3af',
  },
  spacing: lightTheme.spacing,
  radii: lightTheme.radii,
};

// Components access theme via props.theme
const PageWrapper = styled.div`
  background: ${(props) => props.theme.colors.bg};
  color: ${(props) => props.theme.colors.text};
  min-height: 100vh;
  padding: ${(props) => props.theme.spacing.lg};
`;

const Card = styled.div`
  border: 1px solid ${({ theme }) => theme.colors.border};
  border-radius: ${({ theme }) => theme.radii.lg};
  padding: ${({ theme }) => theme.spacing.md};
`;

// Reusable style mixin
const truncate = css`
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
`;

const Title = styled.h2`
  ${truncate}
  color: ${({ theme }) => theme.colors.text};
  font-size: 18px;
`;

// App with theme provider
function App() {
  const [isDark, setIsDark] = useState(false);

  return (
    <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
      <PageWrapper>
        <button onClick={() => setIsDark(!isDark)}>
          Toggle Theme
        </button>
        <Card>
          <Title>Themed Card</Title>
          <p>This card respects the current theme.</p>
        </Card>
      </PageWrapper>
    </ThemeProvider>
  );
}

ThemeProvider injects a theme object via React Context that all styled components can access via props.theme. Switching themes re-renders all themed components with new values. The css helper creates reusable style mixins. Destructuring ({ theme }) in template functions is a common shorthand.

Zero-runtime CSS-in-JS with vanilla-extract

// styles.css.ts (vanilla-extract)
import { style, styleVariants, createTheme } from '@vanilla-extract/css';

// Create a theme with typed tokens
export const [themeClass, vars] = createTheme({
  color: {
    bg: '#ffffff',
    text: '#111827',
    primary: '#3b82f6',
  },
  space: {
    sm: '8px',
    md: '16px',
    lg: '24px',
  },
});

// Static styles (extracted at build time)
export const card = style({
  background: vars.color.bg,
  borderRadius: '12px',
  padding: vars.space.md,
  boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
});

export const title = style({
  fontSize: '18px',
  fontWeight: 600,
  color: vars.color.text,
  margin: 0,
});

// Variant styles
export const buttonVariants = styleVariants({
  primary: {
    background: vars.color.primary,
    color: '#fff',
    padding: `${vars.space.sm} ${vars.space.md}`,
    border: 'none',
    borderRadius: '6px',
    cursor: 'pointer',
  },
  secondary: {
    background: '#e5e7eb',
    color: '#374151',
    padding: `${vars.space.sm} ${vars.space.md}`,
    border: 'none',
    borderRadius: '6px',
    cursor: 'pointer',
  },
});

// ---- Component ---- //
// Card.tsx
import { card, title, buttonVariants } from './styles.css';

function Card() {
  return (
    <div className={card}>
      <h3 className={title}>Zero Runtime</h3>
      <p>Styles extracted at build time. No JS runtime cost.</p>
      <button className={buttonVariants.primary}>Save</button>
      <button className={buttonVariants.secondary}>Cancel</button>
    </div>
  );
}

// Build output: standard CSS file with hashed class names
// .styles_card_x7d3k { background: var(--color-bg); ... }
// .styles_title_a2b4c { font-size: 18px; ... }

vanilla-extract compiles .css.ts files at build time, producing standard CSS files. The API uses JavaScript objects instead of template literals, providing full TypeScript type checking. Theme tokens become CSS custom properties. The output is identical to CSS Modules: hashed class names with zero runtime JavaScript.

Runtime vs zero-runtime comparison

/*
  RUNTIME CSS-in-JS (styled-components, Emotion)
  -----------------------------------------------
  Pros:
  + Fully dynamic styles based on any runtime value
  + Automatic critical CSS (only used styles are injected)
  + Familiar template literal syntax
  + Powerful theming via React Context

  Cons:
  - Runtime overhead: serialization + hashing + injection per render
  - Larger bundle size (library code: ~12-15KB gzipped)
  - Incompatible with React Server Components / streaming SSR
  - Can cause style recalculation on every render

  When to use:
  - Existing projects already using styled-components
  - Highly dynamic UIs where styles depend on runtime data
  - Prototyping and small projects where DX matters most
*/

// styled-components (runtime)
const Box = styled.div`
  /* This runs in the browser on every render */
  width: ${props => props.width}px;
  background: ${props => props.color};
`;

/*
  ZERO-RUNTIME CSS-in-JS (vanilla-extract, Linaria, Panda CSS)
  -----------------------------------------------------------
  Pros:
  + Zero runtime cost (static CSS output)
  + Full TypeScript type safety
  + Compatible with Server Components and streaming SSR
  + Smaller bundle size (no library runtime)

  Cons:
  - Cannot use arbitrary runtime values in styles
  - Requires build tool integration
  - Dynamic styles need CSS custom properties or predefined variants
  - Slightly more complex setup

  When to use:
  - New projects prioritizing performance
  - Server Component architectures (Next.js App Router)
  - Large applications where runtime CSS cost accumulates
*/

// vanilla-extract (zero-runtime)
// Dynamic values via CSS custom properties
export const box = style({
  width: 'var(--box-width)',
  background: 'var(--box-color)',
});

// In component: set CSS variables for dynamic values
// <div className={box} style={{ '--box-width': '200px', '--box-color': 'blue' }} />

Runtime libraries generate CSS in the browser, enabling fully dynamic styles but adding performance overhead. Zero-runtime libraries extract CSS at build time, producing static files with no runtime cost. Dynamic values in zero-runtime solutions use CSS custom properties set via inline style attributes.

Key points

Concepts covered

CSS-in-JS, Styled Components, Runtime CSS, Zero-Runtime CSS, Template Literals, Dynamic Styles