Next.js Styling

Next.js provides multiple styling solutions, from traditional CSS to modern CSS-in-JS. This chapter will detail various styling methods and their best practices.

Styling Solutions Overview

Next.js supports the following styling solutions:

  • Global CSS - Traditional global stylesheets
  • CSS Modules - Locally scoped CSS
  • Sass/SCSS - CSS preprocessor
  • CSS-in-JS - styled-components, emotion, etc.
  • Tailwind CSS - Utility-first CSS framework
  • PostCSS - CSS post-processor

Global CSS

Basic Usage

/* app/globals.css */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  line-height: 1.6;
  color: #333;
}

h1, h2, h3, h4, h5, h6 {
  margin-bottom: 0.5rem;
  font-weight: 600;
}

a {
  color: #0070f3;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}

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>
  )
}

CSS Modules

Creating CSS Module

/* components/Button.module.css */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 24px;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
}

.primary {
  background-color: #0070f3;
  color: white;
}

.primary:hover {
  background-color: #0051cc;
}

.secondary {
  background-color: #f4f4f4;
  color: #333;
  border: 1px solid #ddd;
}

.secondary:hover {
  background-color: #e9e9e9;
}

.disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.large {
  padding: 16px 32px;
  font-size: 18px;
}

.small {
  padding: 8px 16px;
  font-size: 14px;
}

Using CSS Module

// components/Button.tsx
import styles from './Button.module.css'
import { clsx } from 'clsx'

interface ButtonProps {
  children: React.ReactNode
  variant?: 'primary' | 'secondary'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
  onClick?: () => void
}

export default function Button({
  children,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  onClick
}: ButtonProps) {
  return (
    <button
      className={clsx(
        styles.button,
        styles[variant],
        size !== 'medium' && styles[size],
        disabled && styles.disabled
      )}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

Dynamic Class Name Composition

// utils/classNames.ts
export function classNames(...classes: (string | undefined | null | false)[]): string {
  return classes.filter(Boolean).join(' ')
}

// Usage example
import styles from './Card.module.css'
import { classNames } from '@/utils/classNames'

export default function Card({ 
  children, 
  elevated = false, 
  className 
}: {
  children: React.ReactNode
  elevated?: boolean
  className?: string
}) {
  return (
    <div className={classNames(
      styles.card,
      elevated && styles.elevated,
      className
    )}>
      {children}
    </div>
  )
}

Sass/SCSS Support

Installing Sass

npm install --save-dev sass

SCSS File Example

// styles/components.scss
$primary-color: #0070f3;
$secondary-color: #f4f4f4;
$border-radius: 6px;
$transition: all 0.2s ease;

@mixin button-base {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: $border-radius;
  cursor: pointer;
  transition: $transition;
  font-weight: 500;
}

@mixin button-size($padding, $font-size) {
  padding: $padding;
  font-size: $font-size;
}

.btn {
  @include button-base;
  @include button-size(12px 24px, 16px);

  &--primary {
    background-color: $primary-color;
    color: white;

    &:hover {
      background-color: darken($primary-color, 10%);
    }
  }

  &--secondary {
    background-color: $secondary-color;
    color: #333;
    border: 1px solid #ddd;

    &:hover {
      background-color: darken($secondary-color, 5%);
    }
  }

  &--large {
    @include button-size(16px 32px, 18px);
  }

  &--small {
    @include button-size(8px 16px, 14px);
  }

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

Using SCSS Module

// components/Button.tsx
import styles from './Button.module.scss'

export default function Button({ variant, size, children, ...props }) {
  const className = [
    styles.btn,
    styles[`btn--${variant}`],
    size !== 'medium' && styles[`btn--${size}`]
  ].filter(Boolean).join(' ')

  return (
    <button className={className} {...props}>
      {children}
    </button>
  )
}

CSS-in-JS

styled-components

npm install styled-components
npm install --save-dev @types/styled-components
// components/StyledButton.tsx
'use client'

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

interface ButtonProps {
  $variant?: 'primary' | 'secondary'
  $size?: 'small' | 'medium' | 'large'
  disabled?: boolean
}

const StyledButton = styled.button<ButtonProps>`
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;

  ${props => props.$size === 'small' && css`
    padding: 8px 16px;
    font-size: 14px;
  `}

  ${props => props.$size === 'medium' && css`
    padding: 12px 24px;
    font-size: 16px;
  `}

  ${props => props.$size === 'large' && css`
    padding: 16px 32px;
    font-size: 18px;
  `}

  ${props => props.$variant === 'primary' && css`
    background-color: #0070f3;
    color: white;

    &:hover {
      background-color: #0051cc;
    }
  `}

  ${props => props.$variant === 'secondary' && css`
    background-color: #f4f4f4;
    color: #333;
    border: 1px solid #ddd;

    &:hover {
      background-color: #e9e9e9;
    }
  `}

  ${props => props.disabled && css`
    opacity: 0.6;
    cursor: not-allowed;
  `}
`

export default function Button({
  children,
  variant = 'primary',
  size = 'medium',
  ...props
}: {
  children: React.ReactNode
  variant?: 'primary' | 'secondary'
  size?: 'small' | 'medium' | 'large'
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
  return (
    <StyledButton $variant={variant} $size={size} {...props}>
      {children}
    </StyledButton>
  )
}

Theme Provider

// providers/ThemeProvider.tsx
'use client'

import { ThemeProvider as StyledThemeProvider } from 'styled-components'

const theme = {
  colors: {
    primary: '#0070f3',
    secondary: '#f4f4f4',
    text: '#333',
    background: '#fff',
    border: '#ddd'
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px'
  },
  borderRadius: '6px',
  transition: 'all 0.2s ease'
}

export type Theme = typeof theme

export default function ThemeProvider({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <StyledThemeProvider theme={theme}>
      {children}
    </StyledThemeProvider>
  )
}

Using Theme

// components/ThemedButton.tsx
'use client'

import styled from 'styled-components'
import { Theme } from '@/providers/ThemeProvider'

const Button = styled.button<{ $variant: 'primary' | 'secondary' }>`
  padding: ${({ theme }) => `${theme.spacing.md} ${theme.spacing.lg}`};
  border-radius: ${({ theme }) => theme.borderRadius};
  transition: ${({ theme }) => theme.transition};
  border: none;
  cursor: pointer;

  background-color: ${({ theme, $variant }) => 
    $variant === 'primary' ? theme.colors.primary : theme.colors.secondary
  };
  
  color: ${({ theme, $variant }) => 
    $variant === 'primary' ? 'white' : theme.colors.text
  };

  &:hover {
    opacity: 0.8;
  }
`

export default Button

Tailwind CSS

Installation and Configuration

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        }
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
    },
  },
  plugins: [],
}

Base Styles

/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  html {
    font-family: 'Inter', system-ui, sans-serif;
  }
}

@layer components {
  .btn {
    @apply inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md transition-colors duration-200;
  }

  .btn-primary {
    @apply bg-primary-600 text-white hover:bg-primary-700 focus:ring-2 focus:ring-primary-500 focus:ring-offset-2;
  }

  .btn-secondary {
    @apply bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2;
  }

  .card {
    @apply bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden;
  }
}

Tailwind Components

// components/TailwindButton.tsx
import { clsx } from 'clsx'

interface ButtonProps {
  children: React.ReactNode
  variant?: 'primary' | 'secondary' | 'outline'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  onClick?: () => void
}

const variants = {
  primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
  secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500',
  outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-blue-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 default function Button({
  children,
  variant = 'primary',
  size = 'md',
  disabled = false,
  onClick
}: ButtonProps) {
  return (
    <button
      className={clsx(
        'inline-flex items-center justify-center font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2',
        variants[variant],
        sizes[size],
        disabled && 'opacity-50 cursor-not-allowed'
      )}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

Responsive Design

// components/ResponsiveGrid.tsx
export default function ResponsiveGrid({ children }: { children: React.ReactNode }) {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6 lg:gap-8">
      {children}
    </div>
  )
}

// Usage example
export default function ProductGrid({ products }) {
  return (
    <ResponsiveGrid>
      {products.map(product => (
        <div key={product.id} className="card p-6">
          <img 
            src={product.image} 
            alt={product.name}
            className="w-full h-48 object-cover mb-4 rounded"
          />
          <h3 className="text-lg font-semibold mb-2">{product.name}</h3>
          <p className="text-gray-600 mb-4">{product.description}</p>
          <div className="flex items-center justify-between">
            <span className="text-2xl font-bold text-green-600">
              ${product.price}
            </span>
            <button className="btn btn-primary">
              Add to Cart
            </button>
          </div>
        </div>
      ))}
    </ResponsiveGrid>
  )
}

Dynamic Styles

Conditional Styles

// components/StatusBadge.tsx
interface StatusBadgeProps {
  status: 'success' | 'warning' | 'error' | 'info'
  children: React.ReactNode
}

export default function StatusBadge({ status, children }: StatusBadgeProps) {
  const baseClasses = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium'
  
  const statusClasses = {
    success: 'bg-green-100 text-green-800',
    warning: 'bg-yellow-100 text-yellow-800',
    error: 'bg-red-100 text-red-800',
    info: 'bg-blue-100 text-blue-800'
  }

  return (
    <span className={`${baseClasses} ${statusClasses[status]}`}>
      {children}
    </span>
  )
}

CSS Variables

/* app/globals.css */
:root {
  --color-primary: #0070f3;
  --color-secondary: #f4f4f4;
  --spacing-unit: 8px;
  --border-radius: 6px;
}

[data-theme="dark"] {
  --color-primary: #4dabf7;
  --color-secondary: #2d3748;
}

.dynamic-button {
  background-color: var(--color-primary);
  padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3);
  border-radius: var(--border-radius);
}
// components/ThemeToggle.tsx
'use client'

import { useState, useEffect } from 'react'

export default function ThemeToggle() {
  const [theme, setTheme] = useState('light')

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme)
  }, [theme])

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light')
  }

  return (
    <button
      onClick={toggleTheme}
      className="dynamic-button text-white"
    >
      {theme === 'light' ? '🌙' : '☀️'}
    </button>
  )
}

Performance Optimization

CSS Code Splitting

// components/LazyComponent.tsx
import dynamic from 'next/dynamic'

// Dynamically import component and styles
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <p>Loading...</p>,
})

export default function LazyComponent() {
  return (
    <div>
      <h1>Main Content</h1>
      <HeavyComponent />
    </div>
  )
}

Critical CSS Inlining

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <style dangerouslySetInnerHTML={{
          __html: `
            body { margin: 0; font-family: system-ui; }
            .loading { display: flex; justify-content: center; padding: 2rem; }
          `
        }} />
      </head>
      <body>{children}</body>
    </html>
  )
}

Style Debugging

Development Tools

// components/StyleDebugger.tsx
'use client'

import { useState } from 'react'

export default function StyleDebugger({ children }: { children: React.ReactNode }) {
  const [showOutlines, setShowOutlines] = useState(false)

  return (
    <>
      {process.env.NODE_ENV === 'development' && (
        <button
          onClick={() => setShowOutlines(!showOutlines)}
          style={{
            position: 'fixed',
            top: 10,
            right: 10,
            zIndex: 9999,
            padding: '8px 12px',
            background: '#ff6b6b',
            color: 'white',
            border: 'none',
            borderRadius: '4px'
          }}
        >
          {showOutlines ? 'Hide Outlines' : 'Show Outlines'}
        </button>
      )}
      <div className={showOutlines ? 'debug-outlines' : ''}>
        {children}
      </div>
      <style jsx>{`
        .debug-outlines * {
          outline: 1px solid red !important;
        }
      `}</style>
    </>
  )
}

Best Practices

1. Style Organization

styles/
├── globals.css          # Global styles
├── variables.css        # CSS variables
├── components/          # Component styles
│   ├── Button.module.css
│   └── Card.module.css
├── layouts/             # Layout styles
│   └── Header.module.css
└── utilities/           # Utility classes
    └── spacing.css

2. Naming Conventions

/* BEM naming convention */
.card { }
.card__header { }
.card__body { }
.card--elevated { }
.card--compact { }

/* CSS Modules */
.container { }
.title { }
.content { }
.isActive { }
.hasError { }

3. Performance Considerations

// Avoid inline styles
// ❌ Bad
<div style={{ color: 'red', fontSize: '16px' }}>Content</div>

// ✅ Good
<div className={styles.errorText}>Content</div>

// Use CSS variables for theme switching
// ✅ Good
const theme = {
  '--primary-color': isDark ? '#4dabf7' : '#0070f3'
}
<div style={theme}>Content</div>

Summary

Next.js provides rich styling solutions:

  • CSS Modules - Great for component-level styles, avoiding style conflicts
  • Tailwind CSS - Rapid development, consistent design system
  • CSS-in-JS - Dynamic styles, theme switching
  • Sass/SCSS - Complex style logic, variables and mixins
  • Global CSS - Base styles and reset styles

Choosing the right styling solution depends on project requirements, team preferences, and performance needs. It's often recommended to combine multiple solutions to leverage their respective strengths.