Skip to content

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

css
/* 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

typescript
// 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

css
/* 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

typescript
// 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

typescript
// 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

bash
npm install --save-dev sass

SCSS File Example

scss
// 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

typescript
// 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

bash
npm install styled-components
npm install --save-dev @types/styled-components
typescript
// 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

typescript
// 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

typescript
// 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

bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
javascript
// 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

css
/* 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

typescript
// 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

typescript
// 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

typescript
// 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

css
/* 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);
}
typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

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

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

3. Performance Considerations

typescript
// 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.

Content is for learning and research only.