Back to Blog

Next.js CSS Deep Dive: Tailwind, CSS Modules, Sass, and CSS-in-JS

April 1, 202691 min read0 views

Next.js CSS Deep Dive: Tailwind, CSS Modules, Sass, and CSS-in-JS

Introduction

Next.js provides multiple approaches to styling applications, each with distinct tradeoffs for performance, maintainability, and developer experience. This guide covers the internals of each styling approach, CSS ordering behavior, and production patterns for building well-styled Next.js applications.

Styling Architecture Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                      NEXT.JS CSS STYLING OPTIONS                            │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                         COMPILE-TIME CSS                             │   │
│  │                                                                      │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐   │   │
│  │  │ Tailwind CSS │  │ CSS Modules  │  │    Global CSS            │   │   │
│  │  │              │  │              │  │                          │   │   │
│  │  │ Utility-first│  │ Scoped styles│  │ Application-wide         │   │   │
│  │  │ PostCSS      │  │ Hash-based   │  │ Resets, typography       │   │   │
│  │  │ Best perf    │  │ class names  │  │                          │   │   │
│  │  └──────────────┘  └──────────────┘  └──────────────────────────┘   │   │
│  │                                                                      │   │
│  │  ┌──────────────┐  ┌──────────────┐                                 │   │
│  │  │    Sass      │  │  PostCSS     │                                 │   │
│  │  │              │  │  Plugins     │                                 │   │
│  │  │ Variables,   │  │              │                                 │   │
│  │  │ mixins,      │  │ Autoprefixer │                                 │   │
│  │  │ nesting      │  │ Custom       │                                 │   │
│  │  └──────────────┘  └──────────────┘                                 │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                         RUNTIME CSS (CSS-in-JS)                      │   │
│  │                                                                      │   │
│  │  ┌────────────────┐  ┌────────────────┐  ┌────────────────┐        │   │
│  │  │styled-components│  │  styled-jsx   │  │   emotion      │        │   │
│  │  │                │  │               │  │   (WIP)        │        │   │
│  │  │ Template       │  │ Scoped styles │  │                │        │   │
│  │  │ literals       │  │ built-in      │  │                │        │   │
│  │  └────────────────┘  └────────────────┘  └────────────────┘        │   │
│  │                                                                      │   │
│  │  REQUIREMENT: Style registry + useServerInsertedHTML                │   │
│  │  (Client Components only)                                           │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  RECOMMENDATION HIERARCHY:                                                  │
│  1. Tailwind CSS (best performance, most flexible)                         │
│  2. CSS Modules (scoped, no runtime overhead)                              │
│  3. Sass (if you need variables/mixins)                                    │
│  4. CSS-in-JS (if migrating existing codebase)                             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Tailwind CSS

Setup (Tailwind v4)

# Install dependencies
pnpm add -D tailwindcss @tailwindcss/postcss
// postcss.config.mjs
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}
/* app/globals.css */
@import 'tailwindcss';
// app/layout.tsx
import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Tailwind v4 Features

Tailwind v4 uses a CSS-first configuration approach:

/* app/globals.css */
@import 'tailwindcss';

/* Custom theme values using CSS */
@theme {
  --color-brand: #3b82f6;
  --color-brand-dark: #1d4ed8;
  --font-display: 'Inter', sans-serif;
  --breakpoint-3xl: 1920px;
}

/* Plugin imports */
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";

Production Patterns with Tailwind

// components/Button.tsx
import { cn } from '@/lib/utils'

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'destructive' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
}

const variants = {
  primary: 'bg-brand text-white hover:bg-brand-dark focus:ring-brand',
  secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500',
  destructive: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
  ghost: 'bg-transparent hover:bg-gray-100 focus:ring-gray-500',
}

const sizes = {
  sm: 'px-3 py-1.5 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-3 text-lg',
}

export function Button({
  variant = 'primary',
  size = 'md',
  className,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(
        // Base styles
        'inline-flex items-center justify-center rounded-md font-medium',
        'transition-colors duration-200',
        'focus:outline-none focus:ring-2 focus:ring-offset-2',
        'disabled:opacity-50 disabled:cursor-not-allowed',
        // Variant and size
        variants[variant],
        sizes[size],
        // Custom overrides
        className
      )}
      {...props}
    />
  )
}

// lib/utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Conditional and Dynamic Classes

// components/Card.tsx
interface CardProps {
  children: React.ReactNode
  selected?: boolean
  status?: 'success' | 'warning' | 'error'
}

export function Card({ children, selected, status }: CardProps) {
  return (
    <div
      className={cn(
        'rounded-lg border p-4 transition-all',
        // Conditional classes
        selected && 'ring-2 ring-brand border-brand',
        // Dynamic classes based on status
        {
          'border-green-500 bg-green-50': status === 'success',
          'border-yellow-500 bg-yellow-50': status === 'warning',
          'border-red-500 bg-red-50': status === 'error',
        }
      )}
    >
      {children}
    </div>
  )
}

CSS Modules

Basic Usage

CSS Modules generate unique class names, providing automatic scoping without runtime overhead.

/* components/Dashboard.module.css */
.container {
  display: grid;
  grid-template-columns: 250px 1fr;
  gap: 1.5rem;
  min-height: 100vh;
}

.sidebar {
  background: var(--color-gray-50);
  border-right: 1px solid var(--color-gray-200);
  padding: 1.5rem;
}

.content {
  padding: 1.5rem;
}

/* Composing classes */
.header {
  composes: flex items-center justify-between from global;
  margin-bottom: 1.5rem;
}

/* Nested selectors */
.card {
  background: white;
  border-radius: 0.5rem;
  padding: 1rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.card:hover {
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

/* Media queries */
@media (max-width: 768px) {
  .container {
    grid-template-columns: 1fr;
  }

  .sidebar {
    display: none;
  }
}
// components/Dashboard.tsx
import styles from './Dashboard.module.css'

export function Dashboard() {
  return (
    <div className={styles.container}>
      <aside className={styles.sidebar}>
        <nav>{/* Navigation */}</nav>
      </aside>
      <main className={styles.content}>
        <header className={styles.header}>
          <h1>Dashboard</h1>
        </header>
        <div className={styles.card}>
          {/* Content */}
        </div>
      </main>
    </div>
  )
}

TypeScript Support for CSS Modules

// types/css-modules.d.ts
declare module '*.module.css' {
  const classes: { readonly [key: string]: string }
  export default classes
}

declare module '*.module.scss' {
  const classes: { readonly [key: string]: string }
  export default classes
}

For typed CSS Modules with autocomplete:

# Install typed-css-modules
pnpm add -D typed-css-modules

# Generate types
tcm src/

Combining CSS Modules with Tailwind

/* components/Button.module.css */
.button {
  @apply inline-flex items-center justify-center rounded-md font-medium;
  @apply transition-colors duration-200;
  @apply focus:outline-none focus:ring-2 focus:ring-offset-2;
}

.primary {
  @apply bg-blue-600 text-white hover:bg-blue-700;
}

.secondary {
  @apply bg-gray-100 text-gray-900 hover:bg-gray-200;
}

/* Custom styles not in Tailwind */
.button::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  pointer-events: none;
}
// components/Button.tsx
import styles from './Button.module.css'
import { cn } from '@/lib/utils'

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary'
}

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

Global CSS

Structure

/* app/globals.css */

/* CSS Custom Properties (Design Tokens) */
:root {
  --color-primary: #3b82f6;
  --color-primary-dark: #1d4ed8;
  --color-gray-50: #f9fafb;
  --color-gray-100: #f3f4f6;
  --color-gray-200: #e5e7eb;
  --color-gray-900: #111827;

  --font-sans: 'Inter', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;

  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 1rem;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}

/* Dark mode variables */
[data-theme='dark'] {
  --color-primary: #60a5fa;
  --color-gray-50: #18181b;
  --color-gray-100: #27272a;
  --color-gray-200: #3f3f46;
  --color-gray-900: #fafafa;
}

/* CSS Reset */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-family: var(--font-sans);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  min-height: 100vh;
  background: var(--color-gray-50);
  color: var(--color-gray-900);
}

/* Typography */
h1, h2, h3, h4, h5, h6 {
  font-weight: 600;
  line-height: 1.25;
}

h1 { font-size: 2.25rem; }
h2 { font-size: 1.875rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }

p {
  margin-bottom: 1rem;
}

a {
  color: var(--color-primary);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* Utility classes */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

Import in Root Layout

// app/layout.tsx
import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Sass Integration

Setup

pnpm add -D sass

Configuration

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  sassOptions: {
    // Prepend content to every Sass file
    additionalData: `
      @use 'styles/variables' as *;
      @use 'styles/mixins' as *;
    `,
    // Use faster Sass implementation
    implementation: 'sass-embedded',
  },
}

export default nextConfig

Sass Variables and Mixins

// styles/_variables.scss
$color-primary: #3b82f6;
$color-primary-dark: #1d4ed8;
$color-gray-50: #f9fafb;
$color-gray-900: #111827;

$breakpoint-sm: 640px;
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
$breakpoint-xl: 1280px;

$font-sans: 'Inter', system-ui, sans-serif;
$font-mono: 'JetBrains Mono', monospace;

$spacing-unit: 0.25rem;

// styles/_mixins.scss
@mixin responsive($breakpoint) {
  @media (min-width: $breakpoint) {
    @content;
  }
}

@mixin flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

@mixin truncate($lines: 1) {
  @if $lines == 1 {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  } @else {
    display: -webkit-box;
    -webkit-line-clamp: $lines;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

@mixin button-variant($bg, $text, $hover-bg) {
  background-color: $bg;
  color: $text;

  &:hover:not(:disabled) {
    background-color: $hover-bg;
  }

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

Using Sass with CSS Modules

// components/Card.module.scss
@use 'styles/variables' as *;
@use 'styles/mixins' as *;

.card {
  background: white;
  border-radius: 0.5rem;
  padding: $spacing-unit * 4;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  transition: box-shadow 0.2s ease;

  &:hover {
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }

  @include responsive($breakpoint-md) {
    padding: $spacing-unit * 6;
  }
}

.title {
  @include truncate(2);
  font-size: 1.25rem;
  font-weight: 600;
  color: $color-gray-900;
  margin-bottom: $spacing-unit * 2;
}

.content {
  color: lighten($color-gray-900, 30%);
  line-height: 1.6;
}

Exporting Sass Variables to JavaScript

// styles/variables.module.scss
$primary-color: #3b82f6;
$secondary-color: #6366f1;
$breakpoint-md: 768px;

:export {
  primaryColor: $primary-color;
  secondaryColor: $secondary-color;
  breakpointMd: $breakpoint-md;
}
// components/DynamicStyles.tsx
import variables from '@/styles/variables.module.scss'

export function DynamicStyles() {
  // Use Sass variables in JavaScript
  return (
    <div style={{ color: variables.primaryColor }}>
      Styled with Sass variable
    </div>
  )
}

CSS-in-JS

styled-components Setup

pnpm add styled-components
pnpm add -D @types/styled-components
// next.config.js
module.exports = {
  compiler: {
    styledComponents: true,
  },
}
// lib/registry.tsx
'use client'

import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'

export default function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode
}) {
  // Only create stylesheet once with lazy initial state
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement()
    styledComponentsStyleSheet.instance.clearTag()
    return <>{styles}</>
  })

  if (typeof window !== 'undefined') return <>{children}</>

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  )
}
// app/layout.tsx
import StyledComponentsRegistry from './lib/registry'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
      </body>
    </html>
  )
}

styled-components Usage

// components/StyledButton.tsx
'use client'

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

interface ButtonProps {
  $variant?: 'primary' | 'secondary'
  $size?: 'sm' | 'md' | 'lg'
}

const sizes = {
  sm: css`
    padding: 0.375rem 0.75rem;
    font-size: 0.875rem;
  `,
  md: css`
    padding: 0.5rem 1rem;
    font-size: 1rem;
  `,
  lg: css`
    padding: 0.75rem 1.5rem;
    font-size: 1.125rem;
  `,
}

const variants = {
  primary: css`
    background-color: #3b82f6;
    color: white;

    &:hover:not(:disabled) {
      background-color: #2563eb;
    }
  `,
  secondary: css`
    background-color: #e5e7eb;
    color: #1f2937;

    &:hover:not(:disabled) {
      background-color: #d1d5db;
    }
  `,
}

export const Button = styled.button<ButtonProps>`
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 0.375rem;
  font-weight: 500;
  border: none;
  cursor: pointer;
  transition: background-color 0.2s ease;

  ${({ $size = 'md' }) => sizes[$size]}
  ${({ $variant = 'primary' }) => variants[$variant]}

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

  &:focus {
    outline: 2px solid #3b82f6;
    outline-offset: 2px;
  }
`

styled-jsx Setup

// lib/styled-jsx-registry.tsx
'use client'

import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'

export default function StyledJsxRegistry({
  children,
}: {
  children: React.ReactNode
}) {
  const [jsxStyleRegistry] = useState(() => createStyleRegistry())

  useServerInsertedHTML(() => {
    const styles = jsxStyleRegistry.styles()
    jsxStyleRegistry.flush()
    return <>{styles}</>
  })

  return <StyleRegistry registry={jsxStyleRegistry}>{children}</StyleRegistry>
}

styled-jsx Usage

// components/Card.tsx
'use client'

export function Card({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className="card">
      <h2 className="title">{title}</h2>
      <div className="content">{children}</div>

      <style jsx>{`
        .card {
          background: white;
          border-radius: 0.5rem;
          padding: 1.5rem;
          box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        }

        .card:hover {
          box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        .title {
          font-size: 1.25rem;
          font-weight: 600;
          margin-bottom: 1rem;
        }

        .content {
          color: #4b5563;
        }

        @media (max-width: 768px) {
          .card {
            padding: 1rem;
          }
        }
      `}</style>
    </div>
  )
}

CSS Ordering and Chunking

Understanding CSS Order

┌─────────────────────────────────────────────────────────────────────────────┐
│                           CSS ORDERING RULES                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Import order determines CSS cascade order                                  │
│                                                                             │
│  // page.tsx                                                                │
│  import { BaseButton } from './base-button'  // CSS from this loads FIRST  │
│  import styles from './page.module.css'       // CSS from this loads SECOND│
│                                                                             │
│  RESULT:                                                                    │
│  ┌──────────────────────────────────────────────────────────────────────┐  │
│  │  1. base-button.module.css (higher specificity wins on equal weight) │  │
│  │  2. page.module.css                                                   │  │
│  └──────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│  DEVELOPMENT vs PRODUCTION:                                                 │
│  ┌────────────────────────────┬────────────────────────────────────────┐   │
│  │  Development               │  Production                            │   │
│  ├────────────────────────────┼────────────────────────────────────────┤   │
│  │  Hot Module Replacement    │  Concatenated + minified               │   │
│  │  Instant updates           │  Code-split .css files                 │   │
│  │  May differ from prod      │  Final CSS order                       │   │
│  │                            │                                        │   │
│  │  ALWAYS verify in `build`  │  This is what users see                │   │
│  └────────────────────────────┴────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

CSS Chunking Configuration

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Control CSS chunking behavior
  cssChunking: 'loose',  // 'strict' | 'loose'
  // 'strict': More aggressive chunking, smaller per-route CSS
  // 'loose': Larger chunks, potentially fewer network requests
}

export default nextConfig

Best Practices for Predictable CSS Order

// ✓ GOOD: Single entry point for imports
// app/layout.tsx
import '@/styles/globals.css'       // Global styles first
import '@/styles/typography.css'    // Then typography
import '@/styles/components.css'    // Then component base styles

export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  )
}

// ✓ GOOD: Consistent import order in components
// components/Button.tsx
import styles from './Button.module.css'  // Component styles
import { cn } from '@/lib/utils'           // Utilities after styles

// ✗ BAD: Auto-sorted imports that change CSS order
// ESLint sort-imports or Prettier can reorder these
import styles from './page.module.css'
import { Button } from './Button'  // Button's CSS now loads after page!

Disable Import Sorting for CSS

// .eslintrc.js
module.exports = {
  rules: {
    'sort-imports': 'off',
    'import/order': ['error', {
      groups: [
        'builtin',
        'external',
        'internal',
        ['parent', 'sibling'],
        'index',
        'object',
        'type',
      ],
      // Keep CSS imports in their original order
      pathGroups: [
        {
          pattern: '*.css',
          group: 'index',
          position: 'after',
        },
      ],
    }],
  },
}

External Stylesheets

Importing from node_modules

// app/layout.tsx
import 'bootstrap/dist/css/bootstrap.css'
import '@fontsource/inter/400.css'
import '@fontsource/inter/600.css'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  )
}
// app/layout.tsx
import { link } from 'react-dom/components'

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <link
          rel="stylesheet"
          href="https://cdn.example.com/styles.css"
          precedence="default"
        />
      </head>
      <body>{children}</body>
    </html>
  )
}

Complete Application Structure

┌─────────────────────────────────────────────────────────────────────────────┐
│                      RECOMMENDED CSS STRUCTURE                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  app/                                                                       │
│  ├── globals.css           ← Global styles, Tailwind import                │
│  ├── layout.tsx            ← Imports globals.css                           │
│  │                                                                          │
│  ├── (marketing)/                                                           │
│  │   ├── marketing.css     ← Route-group specific styles                   │
│  │   └── layout.tsx        ← Imports marketing.css                         │
│  │                                                                          │
│  └── dashboard/                                                             │
│      ├── dashboard.css     ← Dashboard-specific styles                     │
│      └── layout.tsx        ← Imports dashboard.css                         │
│                                                                             │
│  components/                                                                │
│  ├── ui/                                                                    │
│  │   ├── Button/                                                            │
│  │   │   ├── Button.tsx                                                     │
│  │   │   ├── Button.module.css  ← Scoped component styles                  │
│  │   │   └── index.ts                                                       │
│  │   └── Card/                                                              │
│  │       ├── Card.tsx                                                       │
│  │       ├── Card.module.css                                                │
│  │       └── index.ts                                                       │
│  │                                                                          │
│  └── features/                                                              │
│      └── Dashboard/                                                         │
│          ├── Dashboard.tsx                                                  │
│          ├── Dashboard.module.css                                           │
│          └── index.ts                                                       │
│                                                                             │
│  styles/                                                                    │
│  ├── _variables.scss       ← Sass variables (if using Sass)               │
│  ├── _mixins.scss          ← Sass mixins                                   │
│  └── variables.module.scss ← Exported Sass variables for JS               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Styling Decision Matrix

┌─────────────────────────────────────────────────────────────────────────────┐
│                         STYLING DECISION MATRIX                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  SCENARIO                              RECOMMENDATION                       │
│  ─────────────────────────────────────────────────────────────────────────  │
│  New project, performance critical     Tailwind CSS                         │
│  Need design system utilities          Tailwind CSS                         │
│  Component library                     CSS Modules + Tailwind               │
│  Complex animations                    CSS Modules + Tailwind               │
│  Theming support                       CSS Variables + Tailwind             │
│  Design tokens from Figma              CSS Variables + Tailwind             │
│  Legacy codebase migration             Keep existing + add Tailwind         │
│  Server Components styling             Tailwind or CSS Modules              │
│  Client Components with dynamic        CSS-in-JS or Tailwind                │
│  Third-party CSS frameworks            Import in layout                     │
│                                                                             │
│  PERFORMANCE RANKING (Best to Worst):                                       │
│  1. Tailwind CSS (purged, minimal CSS)                                     │
│  2. CSS Modules (scoped, no runtime)                                       │
│  3. Global CSS (simple, but can grow)                                      │
│  4. Sass (adds compile step)                                               │
│  5. CSS-in-JS (runtime overhead, SSR complexity)                           │
│                                                                             │
│  BUNDLE SIZE IMPACT:                                                        │
│  ┌──────────────────────┬───────────────────────────────────────────────┐  │
│  │  Approach            │  Impact                                       │  │
│  ├──────────────────────┼───────────────────────────────────────────────┤  │
│  │  Tailwind            │  ~10KB (purged, compressed)                   │  │
│  │  CSS Modules         │  Minimal (only used classes)                  │  │
│  │  Global CSS          │  Varies (all styles loaded)                   │  │
│  │  styled-components   │  ~15KB runtime + styles                       │  │
│  │  emotion             │  ~11KB runtime + styles                       │  │
│  └──────────────────────┴───────────────────────────────────────────────┘  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Key Takeaways

  1. Tailwind First: Use Tailwind CSS as the primary styling approach for best performance and DX
  2. CSS Modules for Scoping: Use CSS Modules when you need custom CSS with automatic scoping
  3. Global CSS Sparingly: Limit global CSS to truly global styles (resets, typography, CSS variables)
  4. Sass for Variables/Mixins: Use Sass when you need variables, mixins, or nesting beyond CSS capabilities
  5. CSS-in-JS Carefully: Only use CSS-in-JS if migrating existing codebase or need runtime styling
  6. Verify in Production: CSS order can differ between dev and build - always verify final build
  7. Disable Auto-Sort: Turn off ESLint/Prettier import sorting for CSS to maintain order
  8. Use CSS Variables: Prefer CSS custom properties for theming over Sass variables
  9. Co-locate Styles: Keep component CSS modules next to component files
  10. Single Entry Point: Import global styles in root layout for predictable ordering

References

What did you think?

Related Posts

© 2026 Vidhya Sagar Thakur. All rights reserved.