NextJS DOC
Part 10 of 151. Next.js Project Structure: A Comprehensive Architecture Guide2. Next.js Layouts and Pages: Complete Architecture Guide3. Next.js Linking and Navigation: Complete Architecture Guide4. Next.js Server and Client Components: Complete Architecture Guide5. Next.js Data Fetching: Complete Architecture Guide6. Next.js Data Mutation: Complete Server Actions Guide7. Next.js Caching Deep Dive: Cache Components and the `use cache` Directive8. Next.js Revalidation Deep Dive: Time-Based and On-Demand Cache Invalidation9. Next.js Error Handling Deep Dive: Expected Errors, Uncaught Exceptions, and Recovery Patterns10. Next.js CSS Deep Dive: Tailwind, CSS Modules, Sass, and CSS-in-JS11. Next.js Image Optimization Deep Dive: Performance, Responsive Images, and Configuration12. Next.js Font Optimization Deep Dive: Self-Hosted Fonts, CSS Variables, and CLS Prevention13. Next.js Metadata and OG Images Deep Dive: SEO, Social Sharing, and Dynamic Generation14. Next.js Route Handlers Deep Dive: Building Production APIs with Web Standards15. Next.js Proxy Deep Dive: Edge-First Request Interception
Next.js CSS Deep Dive: Tailwind, CSS Modules, Sass, and CSS-in-JS
April 1, 202691 min read0 views
nextjs
css architecture
tailwindcss
css modules
sass
css in js
frontend architecture
scalable frontend
web performance
developer experience
styling strategies
modern web
react
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>
)
}
React 19 Link Component
// 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
- Tailwind First: Use Tailwind CSS as the primary styling approach for best performance and DX
- CSS Modules for Scoping: Use CSS Modules when you need custom CSS with automatic scoping
- Global CSS Sparingly: Limit global CSS to truly global styles (resets, typography, CSS variables)
- Sass for Variables/Mixins: Use Sass when you need variables, mixins, or nesting beyond CSS capabilities
- CSS-in-JS Carefully: Only use CSS-in-JS if migrating existing codebase or need runtime styling
- Verify in Production: CSS order can differ between dev and build - always verify final build
- Disable Auto-Sort: Turn off ESLint/Prettier import sorting for CSS to maintain order
- Use CSS Variables: Prefer CSS custom properties for theming over Sass variables
- Co-locate Styles: Keep component CSS modules next to component files
- Single Entry Point: Import global styles in root layout for predictable ordering
References
What did you think?
Related Posts
April 4, 202697 min
Next.js Proxy Deep Dive: Edge-First Request Interception
nextjs
proxy
April 4, 2026164 min
Next.js Route Handlers Deep Dive: Building Production APIs with Web Standards
nextjs
route handlers
April 4, 202691 min
Next.js Metadata and OG Images Deep Dive: SEO, Social Sharing, and Dynamic Generation
nextjs
seo