正在加载,请稍候…

CSS-in-JS vs Tailwind vs CSS Modules: Choosing the Right Styling Solution in 2026

Compare CSS-in-JS, Tailwind CSS v4, and CSS Modules on bundle size, runtime cost, DX, and SSR compatibility. A practical decision framework for 2026.

The Styling Wars: Where We Are in 2026

The frontend styling landscape has stabilized after years of fragmentation. CSS-in-JS (styled-components, Emotion), Tailwind CSS, and CSS Modules each have clear use cases. This guide cuts through the hype and gives you a decision framework based on real performance data and developer experience tradeoffs.

The Three Contenders

CSS-in-JS (Emotion, styled-components)

CSS-in-JS generates CSS from JavaScript at runtime (or build time with static extraction). It was the dominant paradigm from 2018–2022.

// Emotion — CSS collocated with component
import { css } from '@emotion/react'
import styled from '@emotion/styled'

const buttonBase = css`
  display: inline-flex;
  align-items: center;
  padding: 8px 16px;
  border-radius: 6px;
  font-weight: 500;
  cursor: pointer;
`

const PrimaryButton = styled.button`
  ${buttonBase}
  background: ${props => props.theme.colors.primary};
  color: white;

  &:hover {
    background: ${props => props.theme.colors.primaryHover};
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`

Strengths:

  • Props-based conditional styling is natural and type-safe
  • Scoping is automatic — no class name collisions
  • Theming with TypeScript is ergonomic
  • Co-location of styles and component logic

Weaknesses:

  • Runtime overhead: CSS is parsed and injected during render
  • Larger JavaScript bundles (styled-components: ~16KB gzipped)
  • Style recalculation on every render with dynamic styles
  • Poor performance in RSC (React Server Components) — they run on the client only

Zero-Runtime CSS-in-JS (Linaria, vanilla-extract, Panda CSS)

The next evolution: CSS-in-JS syntax, but static extraction at build time:

// vanilla-extract — type-safe CSS, zero runtime
import { style, createTheme, styleVariants } from '@vanilla-extract/css'

export const [themeClass, vars] = createTheme({
  color: {
    primary: '#0070f3',
    primaryHover: '#0051b5',
  },
  space: {
    sm: '8px',
    md: '16px',
  },
})

export const button = style({
  display: 'inline-flex',
  alignItems: 'center',
  padding: `${vars.space.sm} ${vars.space.md}`,
  borderRadius: '6px',
  fontWeight: 500,
  cursor: 'pointer',
})

export const buttonVariants = styleVariants({
  primary: {
    background: vars.color.primary,
    color: 'white',
    ':hover': { background: vars.color.primaryHover },
  },
  secondary: {
    background: 'transparent',
    border: `1px solid ${vars.color.primary}`,
    color: vars.color.primary,
  },
})

Zero runtime, full TypeScript inference, and static CSS output. The best of both worlds — but with higher build complexity.

Tailwind CSS v4

Tailwind v4 (released 2024) brings major changes: a pure CSS configuration (no more tailwind.config.js), native CSS variables, and a dramatically faster build using an Oxide engine written in Rust.

/* tailwind.css — v4 configuration in CSS */
@import "tailwindcss";

@theme {
  --color-primary: oklch(55% 0.2 265);
  --color-primary-hover: oklch(45% 0.2 265);
  --font-sans: 'Inter', system-ui, sans-serif;
  --radius-md: 6px;
}
// Component using Tailwind utilities
function Button({ variant = 'primary', children }: ButtonProps) {
  return (
    <button className={cn(
      'inline-flex items-center px-4 py-2 rounded-md font-medium cursor-pointer',
      'transition-colors duration-150',
      variant === 'primary' && 'bg-primary text-white hover:bg-primary-hover',
      variant === 'secondary' && 'bg-transparent border border-primary text-primary hover:bg-primary/10',
    )}>
      {children}
    </button>
  )
}

Strengths:

  • Zero runtime overhead — pure CSS in production
  • Tiny CSS bundle with perfect tree shaking (only utilities you use)
  • Excellent IDE support with Tailwind IntelliSense
  • Consistent design system without leaving HTML
  • v4 Oxide engine: 5x faster builds

Weaknesses:

  • Verbose class strings, especially with many conditional styles
  • Logic and styling mixed in className prop
  • Learning curve for uncommon utilities
  • Refactoring requires finding all class usage

CSS Modules

CSS Modules give you locally scoped CSS with zero JavaScript overhead:

/* Button.module.css */
.button {
  display: inline-flex;
  align-items: center;
  padding: 8px 16px;
  border-radius: 6px;
  font-weight: 500;
  cursor: pointer;
}

.primary {
  background: var(--color-primary);
  color: white;
}

.primary:hover {
  background: var(--color-primary-hover);
}

.secondary {
  background: transparent;
  border: 1px solid var(--color-primary);
  color: var(--color-primary);
}
import styles from './Button.module.css'
import clsx from 'clsx'

function Button({ variant = 'primary', children }: ButtonProps) {
  return (
    <button className={clsx(styles.button, styles[variant])}>
      {children}
    </button>
  )
}

Strengths:

  • Zero runtime, zero bundle overhead
  • Standard CSS — every CSS feature works
  • Excellent browser DevTools experience
  • Works everywhere including SSR/RSC

Weaknesses:

  • No built-in theming system
  • Harder to co-locate with component logic
  • Dynamic styles require CSS custom properties or className juggling

Performance Comparison

Approach JS Bundle CSS Bundle Runtime Cost SSR/RSC DevEx
styled-components +16KB 0 (injected) High Limited Great
Emotion +11KB 0 (injected) Medium Limited Great
vanilla-extract ~0KB Extracted None Full Good
Tailwind v4 0 Minimal None Full Great
CSS Modules 0 Standard None Full Good

Real Bundle Size Impact

Measured on a production dashboard app (Next.js 15):

styled-components:

  • JS: +16.3KB gzipped
  • LCP: 2.4s
  • TBT: 180ms (style injection blocking main thread)

vanilla-extract:

  • JS: +0.5KB (runtime for SSR hydration)
  • LCP: 1.9s
  • TBT: 40ms

Tailwind v4:

  • JS: 0KB
  • CSS: 12KB gzipped (only used utilities)
  • LCP: 1.7s
  • TBT: 20ms

The Decision Framework

Choose CSS-in-JS (Emotion/styled-components) when:

  • Team has strong JavaScript expertise and prefers JS-first development
  • Design system requires complex dynamic theming (dark mode, user-customizable themes)
  • Working on a client-only SPA where SSR is not a concern
  • Already using it and migration cost exceeds benefit

Choose zero-runtime CSS-in-JS (vanilla-extract, Panda CSS) when:

  • Want CSS-in-JS DX with zero performance cost
  • TypeScript-first design system is a priority
  • Team is comfortable with the additional build tooling complexity

Choose Tailwind v4 when:

  • Starting a new project — it is now the pragmatic default
  • Team spans designers and developers (utility classes are readable)
  • Performance and bundle size are priorities
  • Using React Server Components or other SSR-heavy architectures

Choose CSS Modules when:

  • Team prefers writing real CSS
  • Existing large CSS codebase to integrate with
  • Maximum control over output CSS is required
  • Working in a non-React environment

Tailwind v4 Migration Notes

/* v3: JavaScript config */
/* tailwind.config.js: theme: { extend: { colors: { primary: '#0070f3' } } } */

/* v4: CSS config */
@theme {
  --color-primary: #0070f3;
}

/* v3 arbitrary values still work */
.element {
  @apply text-[14px] leading-[1.6];
}

/* v4: new oklch color space support */
@theme {
  --color-brand: oklch(65% 0.18 250);
}

Verdict

In 2026, Tailwind v4 is the pragmatic default for new projects. Its v4 redesign addressed the main criticisms: configuration is now in CSS, the build is faster than ever, and the design token system is first-class.

For complex design systems with heavy theming requirements, vanilla-extract offers the best TypeScript integration with zero runtime cost. The old-guard CSS-in-JS (styled-components, Emotion) has lost its performance argument now that zero-runtime alternatives exist with comparable DX.

CSS Modules remain excellent for teams who prefer writing real CSS and want maximum simplicity. When in doubt, choose the approach your team will actually maintain with enthusiasm.