Comprehensive Styling Approaches for Next.js
Comprehensive styling approaches for Next.js. Covers design tokens (JSON, Style Dictionary, CSS variables), CSS Modules, CSS-in-JS solutions (styled-components, vanilla-extract, Panda CSS), global styles, and when to use which approach.
License: MIT Author: Bala Version: 0.1.0
Overview: Styling Options in Next.js
Next.js provides multiple ways to style your application. Choose based on your project needs:
| Approach | Use Case | Bundle Size | Server Components |
|---|---|---|---|
| Tailwind CSS | Rapid prototyping, utility-first | Smallest | Full support |
| CSS Modules | Component-scoped styles | Small | Full support |
| vanilla-extract | Type-safe, design systems | Zero-runtime | Full support |
| Panda CSS | Build-time CSS-in-JS | Small | Full support |
| styled-components | Dynamic theming, runtime CSS | Medium | Limited ("use client") |
| CSS variables | Token-based theming | Minimal | Full support |
Tailwind CSS (Recommended for Most Projects)
See tailwindcss skill file for complete Tailwind documentation.
Best for:
- •Rapid development
- •Design consistency
- •Responsive design
- •Dark mode
- •Teams familiar with utility-first CSS
Setup:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p
CSS Modules
Built-in support with Next.js. Perfect for scoped, component-level styling.
Basic Usage
/* Button.module.css */
.button {
padding: 8px 16px;
border-radius: 4px;
background-color: #3b82f6;
color: white;
border: none;
cursor: pointer;
font-weight: 500;
}
.button:hover {
background-color: #2563eb;
}
.primary {
composes: button;
background-color: #3b82f6;
}
.secondary {
composes: button;
background-color: #6b7280;
}
// Button.tsx
import styles from './Button.module.css'
interface ButtonProps {
variant?: 'primary' | 'secondary'
children: React.ReactNode
}
export function Button({ variant = 'primary', children }: ButtonProps) {
const className = variant === 'primary' ? styles.primary : styles.secondary
return <button className={className}>{children}</button>
}
Naming Conventions
camelCase (Recommended):
.buttonPrimary { /* ... */ }
.buttonSecondary { /* ... */ }
import styles from './Button.module.css' styles.buttonPrimary // accessed as property
kebab-case:
.button-primary { /* ... */ }
import styles from './Button.module.css' styles['button-primary'] // accessed with brackets
Composition with composes
/* base.module.css */
.base {
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
}
/* Button.module.css */
.primary {
composes: base from './base.module.css';
background-color: #3b82f6;
color: white;
}
Global Selectors
/* Container.module.css */
.container {
padding: 20px;
}
.container :global(p) {
margin-bottom: 12px;
}
.container :global(a) {
color: #3b82f6;
text-decoration: none;
}
Dynamic Class Names (with clsx/cn)
import clsx from 'clsx'
import styles from './Button.module.css'
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger'
size: 'sm' | 'md' | 'lg'
disabled?: boolean
}
export function Button({ variant, size, disabled }: ButtonProps) {
const className = clsx(
styles.button,
{
[styles.primary]: variant === 'primary',
[styles.secondary]: variant === 'secondary',
[styles.danger]: variant === 'danger',
[styles.small]: size === 'sm',
[styles.medium]: size === 'md',
[styles.large]: size === 'lg',
[styles.disabled]: disabled,
}
)
return <button className={className} disabled={disabled} />
}
CSS Modules with TypeScript
npm install -D typed-css-modules
Design Tokens
Design tokens are the foundation of scalable, consistent design systems.
Token File Format (JSON)
{
"colors": {
"primary": "#3b82f6",
"secondary": "#6b7280",
"success": "#10b981",
"danger": "#ef4444",
"warning": "#f59e0b",
"light": "#f3f4f6",
"dark": "#1f2937"
},
"spacing": {
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "32px",
"2xl": "48px"
},
"typography": {
"fontSize": {
"xs": "12px",
"sm": "14px",
"base": "16px",
"lg": "18px",
"xl": "20px",
"2xl": "24px"
},
"fontWeight": {
"light": 300,
"normal": 400,
"medium": 500,
"semibold": 600,
"bold": 700
}
}
}
CSS Custom Properties (Variables)
/* globals.css */
:root {
/* Colors */
--color-primary: #3b82f6;
--color-secondary: #6b7280;
--color-success: #10b981;
--color-danger: #ef4444;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Typography */
--font-size-base: 16px;
--font-weight-medium: 500;
--font-weight-bold: 700;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
/* Shadows */
--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 */
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #60a5fa;
--color-secondary: #9ca3af;
}
}
Usage:
.button {
padding: var(--spacing-md);
background-color: var(--color-primary);
border-radius: var(--radius-md);
font-weight: var(--font-weight-medium);
box-shadow: var(--shadow-md);
}
Style Dictionary (Multi-Platform)
For generating tokens across platforms (web, iOS, Android):
npm install -D style-dictionary
See design-tokens.md for setup.
CSS-in-JS Solutions
vanilla-extract (Zero-Runtime, Recommended for App Router)
Setup:
npm install @vanilla-extract/css npm install -D @vanilla-extract/next-plugin
Configuration:
// next.config.ts
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin'
const withVanillaExtract = createVanillaExtractPlugin()
export default withVanillaExtract({})
Usage:
// Button.css.ts
import { style, globalStyle } from '@vanilla-extract/css'
export const button = style({
padding: '8px 16px',
borderRadius: '4px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
cursor: 'pointer',
fontWeight: 500,
':hover': {
backgroundColor: '#2563eb',
},
})
export const primary = style([button, {
backgroundColor: '#3b82f6',
}])
export const secondary = style([button, {
backgroundColor: '#6b7280',
}])
// Button.tsx
import * as styles from './Button.css'
export function Button({ variant = 'primary' }) {
const className = variant === 'primary' ? styles.primary : styles.secondary
return <button className={className}>Click me</button>
}
Advantages:
- •Zero runtime CSS
- •Type-safe style definitions
- •Full server component support
- •CSS output is deterministic and optimized
Panda CSS (Build-Time CSS-in-JS)
Setup:
npm install @pandacss/dev npx panda init
Usage:
// Button.tsx
import { css } from '@pandacss/preset-base'
export function Button() {
return (
<button className={css({
px: '16px',
py: '8px',
bg: 'blue.500',
color: 'white',
rounded: 'md',
fontWeight: 'medium',
_hover: { bg: 'blue.600' }
})}>
Click me
</button>
)
}
Advantages:
- •Responsive design with array syntax
- •Built-in dark mode support
- •No runtime overhead
- •Atomic CSS output
styled-components (Runtime CSS-in-JS)
Setup:
npm install styled-components npm install -D @types/styled-components
Configuration:
// next.config.ts
const config = {
compiler: {
styledComponents: true,
},
}
Usage:
// Button.tsx
'use client'
import styled from 'styled-components'
const StyledButton = styled.button`
padding: 8px 16px;
border-radius: 4px;
background-color: #3b82f6;
color: white;
border: none;
cursor: pointer;
font-weight: 500;
&:hover {
background-color: #2563eb;
}
`
export function Button() {
return <StyledButton>Click me</StyledButton>
}
Limitations:
- •Requires
'use client'directive - •Runtime CSS injection overhead
- •Not ideal for large-scale apps
Global Styles
/* app/globals.css */
@import "tailwindcss";
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
font-size: 16px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.5;
color: var(--color-foreground);
background-color: var(--color-background);
}
/* Typography hierarchy */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.2;
margin-bottom: 1rem;
}
h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }
/* Links */
a {
color: var(--color-primary);
text-decoration: none;
transition: color 0.2s;
}
a:hover {
color: var(--color-primary-dark);
}
Decision Matrix: Which Approach to Use?
Choose Tailwind if:
- •Starting a new project
- •Want rapid prototyping
- •Team knows utility-first CSS
- •Need responsive design out of the box
Choose CSS Modules if:
- •Want component-scoped, isolated styles
- •Prefer traditional CSS
- •Don't need complex theme switching
- •Want zero runtime overhead
Choose vanilla-extract if:
- •Building a design system
- •Want type-safe styles
- •Need zero-runtime CSS
- •Prioritize app performance
Choose Panda CSS if:
- •Want CSS-in-JS with no runtime cost
- •Need responsive design syntax
- •Want modern DX with dev tools
- •Building scalable applications
Choose styled-components if:
- •Need dynamic theming based on state
- •Comfortable with runtime CSS
- •Building component libraries
- •Need full JS-like expressions in styles
Choose CSS variables if:
- •Want lightweight token-based theming
- •Already using CSS or Tailwind
- •Need simple dark mode switching
- •Want minimal bundle size
Combining Approaches
Many projects use multiple approaches:
// components/Card.tsx
'use client'
import styles from './Card.module.css' // CSS Modules for structure
import { classNames } from '@/lib/utils' // utility function
import './Card.global.css' // Global theme-aware styles with CSS variables
export function Card({ children }) {
return (
<div className={classNames(styles.card, 'dark:bg-slate-900')}>
{/* Tailwind for responsive utilities */}
{/* CSS Modules for component structure */}
{/* CSS variables for theme colors */}
{children}
</div>
)
}
This hybrid approach balances:
- •Performance (CSS Modules + Tailwind)
- •Flexibility (CSS variables)
- •DX (utility classes + scoped styles)
Performance Optimization
Bundle Size
- •Tailwind v4: ~8-15kb gzipped (CSS-first, optimized)
- •CSS Modules: ~0kb (native CSS)
- •vanilla-extract: ~0kb (extracted to CSS at build time)
- •Panda CSS: ~5-10kb gzipped (atomic CSS)
- •styled-components: ~12-16kb gzipped (runtime cost)
Server Component Support
| Approach | Server Components | Notes |
|---|---|---|
| Tailwind | Full | ✓ Recommended |
| CSS Modules | Full | ✓ Best practice |
| vanilla-extract | Full | ✓ Zero-runtime |
| Panda CSS | Full | ✓ Type-safe |
| styled-components | Limited | ⚠ Requires "use client" |
Resources
- •Tailwind CSS: https://tailwindcss.com
- •CSS Modules: https://nextjs.org/docs/app/building-your-application/styling/css-modules
- •vanilla-extract: https://vanilla-extract.style
- •Panda CSS: https://panda-css.com
- •styled-components: https://styled-components.com
Next Steps
See the reference files for more detailed patterns:
- •design-tokens.md - Token setup and conventions
- •css-modules-patterns.md - CSS Modules examples