Next.js Complete Guide Part 5: Styling and UI Components - CSS Modules, Tailwind, and More

Next.js Complete Guide Part 5: Styling and UI Components - CSS Modules, Tailwind, and More

Master Next.js styling with CSS Modules, Tailwind CSS, Styled Components, and UI libraries. Learn responsive design, theming, and component styling best practices.

By Next.js Expert
12 min read
2323 words

Welcome to Part 5! Now that you've mastered Next.js fundamentals and full-stack development, let's make your applications beautiful with modern styling techniques and UI components.

Next.js Styling Options Overview

Next.js supports multiple styling approaches, each with unique benefits:

1. CSS Modules - Scoped CSS

  • Automatic class name scoping
  • No style conflicts
  • Great for component-specific styles

2. Global CSS - Traditional approach

  • Site-wide styles
  • CSS resets and base styles
  • Typography and layout

3. Tailwind CSS - Utility-first framework

  • Rapid development
  • Consistent design system
  • Highly customizable

4. Styled Components - CSS-in-JS

  • Dynamic styling
  • Theme support
  • Component-based approach

5. Sass/SCSS - Enhanced CSS

  • Variables and mixins
  • Nested rules
  • Advanced features

CSS Modules - Component Scoped Styles

CSS Modules automatically scope CSS to components, preventing style conflicts.

Basic CSS Modules Setup

Create components/Button/Button.module.css:

.button {
  padding: 12px 24px;
  border: none;
  border-radius: 6px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
  font-size: 16px;
}

.primary {
  background-color: #3b82f6;
  color: white;
}

.primary:hover {
  background-color: #2563eb;
  transform: translateY(-1px);
}

.secondary {
  background-color: #f3f4f6;
  color: #374151;
  border: 1px solid #d1d5db;
}

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

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

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

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

.disabled:hover {
  transform: none;
}

Create components/Button/Button.js:

import styles from './Button.module.css'

export default function Button({ 
  children, 
  variant = 'primary', 
  size = 'medium',
  disabled = false,
  onClick,
  ...props 
}) {
  const buttonClasses = [
    styles.button,
    styles[variant],
    size !== 'medium' && styles[size],
    disabled && styles.disabled
  ].filter(Boolean).join(' ')

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

Use the Button component:

// pages/index.js
import Button from '../components/Button/Button'

export default function Home() {
  return (
    <div>
      <h1>Button Examples</h1>
      
      <Button variant="primary" size="large">
        Primary Large
      </Button>
      
      <Button variant="secondary">
        Secondary Default
      </Button>
      
      <Button variant="primary" size="small" disabled>
        Disabled Small
      </Button>
    </div>
  )
}

Advanced CSS Modules Patterns

Create components/Card/Card.module.css:

.card {
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  transition: all 0.3s ease;
}

.card:hover {
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
  transform: translateY(-2px);
}

.header {
  padding: 24px 24px 0;
}

.title {
  font-size: 1.5rem;
  font-weight: 700;
  color: #1f2937;
  margin: 0 0 8px;
}

.subtitle {
  color: #6b7280;
  margin: 0;
}

.content {
  padding: 16px 24px;
}

.footer {
  padding: 0 24px 24px;
  border-top: 1px solid #f3f4f6;
  margin-top: 16px;
  padding-top: 16px;
}

.actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
}

/* Responsive design */
@media (max-width: 768px) {
  .card {
    margin: 16px;
  }
  
  .header,
  .content,
  .footer {
    padding-left: 16px;
    padding-right: 16px;
  }
  
  .actions {
    flex-direction: column;
  }
}

Global CSS and Layout Styles

Create styles/globals.css:

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

html {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 
               'Helvetica Neue', Arial, sans-serif;
  line-height: 1.6;
  color: #333;
}

body {
  background-color: #fafafa;
  min-height: 100vh;
}

/* Typography */
h1, h2, h3, h4, h5, h6 {
  font-weight: 600;
  line-height: 1.2;
  margin-bottom: 0.5em;
}

h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5rem; }

p {
  margin-bottom: 1rem;
}

a {
  color: #3b82f6;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

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

.grid {
  display: grid;
  gap: 2rem;
}

.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 768px) {
  .grid-2,
  .grid-3,
  .grid-4 {
    grid-template-columns: 1fr;
  }
}

/* Utility Classes */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }

.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mt-8 { margin-top: 2rem; }

.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-8 { margin-bottom: 2rem; }

.hidden { display: none; }
.block { display: block; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }

.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.items-center { align-items: center; }

/* Dark mode support */
@media (prefers-color-scheme: dark) {
  body {
    background-color: #1a1a1a;
    color: #e5e5e5;
  }
  
  .card {
    background-color: #2a2a2a;
    border: 1px solid #404040;
  }
}

Tailwind CSS Integration

Tailwind CSS is a utility-first framework that's perfect for Next.js.

Installing Tailwind CSS

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Configure tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
    './app/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        },
        secondary: {
          50: '#f9fafb',
          500: '#6b7280',
          600: '#4b5563',
        }
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      },
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
      }
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
  ],
}

Update styles/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom component classes */
@layer components {
  .btn {
    @apply px-6 py-3 rounded-lg font-semibold transition-all duration-200;
  }
  
  .btn-primary {
    @apply bg-primary-500 text-white hover:bg-primary-600 
           focus:ring-4 focus:ring-primary-200;
  }
  
  .btn-secondary {
    @apply bg-gray-200 text-gray-800 hover:bg-gray-300 
           focus:ring-4 focus:ring-gray-100;
  }
  
  .card {
    @apply bg-white rounded-xl shadow-lg p-6 
           hover:shadow-xl transition-shadow duration-300;
  }
  
  .input {
    @apply w-full px-4 py-3 border border-gray-300 rounded-lg 
           focus:ring-2 focus:ring-primary-500 focus:border-primary-500;
  }
}

/* Custom utilities */
@layer utilities {
  .text-gradient {
    @apply bg-gradient-to-r from-primary-500 to-purple-600 
           bg-clip-text text-transparent;
  }
}

Tailwind Component Examples

Create components/Hero/Hero.js:

import Link from 'next/link'

export default function Hero() {
  return (
    <section className="bg-gradient-to-br from-primary-50 to-purple-50 py-20">
      <div className="container mx-auto px-6">
        <div className="max-w-4xl mx-auto text-center">
          <h1 className="text-5xl md:text-6xl font-bold text-gray-900 mb-6">
            Build Amazing
            <span className="text-gradient"> Web Apps</span>
          </h1>
          
          <p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
            Create fast, scalable, and beautiful web applications with 
            Next.js and modern development tools.
          </p>
          
          <div className="flex flex-col sm:flex-row gap-4 justify-center">
            <Link href="/get-started" className="btn btn-primary">
              Get Started
            </Link>
            <Link href="/docs" className="btn btn-secondary">
              View Documentation
            </Link>
          </div>
        </div>
      </div>
    </section>
  )
}

Create components/FeatureGrid/FeatureGrid.js:

const features = [
  {
    icon: '⚡',
    title: 'Lightning Fast',
    description: 'Optimized for speed with automatic code splitting and lazy loading.'
  },
  {
    icon: '🔒',
    title: 'Secure by Default',
    description: 'Built-in security features and best practices out of the box.'
  },
  {
    icon: '📱',
    title: 'Mobile First',
    description: 'Responsive design that works perfectly on all devices.'
  },
  {
    icon: '🎨',
    title: 'Customizable',
    description: 'Flexible theming system and component customization.'
  }
]

export default function FeatureGrid() {
  return (
    <section className="py-16 bg-white">
      <div className="container mx-auto px-6">
        <div className="text-center mb-12">
          <h2 className="text-3xl font-bold text-gray-900 mb-4">
            Why Choose Our Platform?
          </h2>
          <p className="text-gray-600 max-w-2xl mx-auto">
            We provide everything you need to build modern web applications
          </p>
        </div>
        
        <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
          {features.map((feature, index) => (
            <div key={index} className="card text-center group hover:scale-105">
              <div className="text-4xl mb-4 group-hover:scale-110 transition-transform">
                {feature.icon}
              </div>
              <h3 className="text-xl font-semibold text-gray-900 mb-3">
                {feature.title}
              </h3>
              <p className="text-gray-600">
                {feature.description}
              </p>
            </div>
          ))}
        </div>
      </div>
    </section>
  )
}

Styled Components - CSS-in-JS

For dynamic styling and theming, Styled Components is excellent.

Installing Styled Components

npm install styled-components
npm install -D babel-plugin-styled-components

Configure .babelrc:

{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true }]]
}

Update pages/_document.js:

import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        })

      const initialProps = await Document.getInitialProps(ctx)
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      }
    } finally {
      sheet.seal()
    }
  }
}

Styled Components Examples

Create components/StyledButton/StyledButton.js:

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

const StyledButton = styled.button`
  padding: ${props => props.size === 'large' ? '16px 32px' : '12px 24px'};
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
  font-size: ${props => props.size === 'large' ? '18px' : '16px'};
  
  ${props => props.variant === 'primary' && css`
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    
    &:hover {
      transform: translateY(-2px);
      box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
    }
  `}
  
  ${props => props.variant === 'secondary' && css`
    background: white;
    color: #4a5568;
    border: 2px solid #e2e8f0;
    
    &:hover {
      border-color: #667eea;
      color: #667eea;
    }
  `}
  
  ${props => props.disabled && css`
    opacity: 0.5;
    cursor: not-allowed;
    
    &:hover {
      transform: none;
      box-shadow: none;
    }
  `}
`

export default function Button({ children, ...props }) {
  return <StyledButton {...props}>{children}</StyledButton>
}

Theme Provider Setup

Create styles/theme.js:

export const lightTheme = {
  colors: {
    primary: '#667eea',
    secondary: '#764ba2',
    background: '#ffffff',
    surface: '#f7fafc',
    text: '#2d3748',
    textSecondary: '#718096',
    border: '#e2e8f0',
    success: '#48bb78',
    warning: '#ed8936',
    error: '#f56565',
  },
  fonts: {
    body: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
    heading: 'Georgia, serif',
    mono: 'Menlo, Monaco, "Courier New", monospace',
  },
  fontSizes: {
    xs: '0.75rem',
    sm: '0.875rem',
    md: '1rem',
    lg: '1.125rem',
    xl: '1.25rem',
    '2xl': '1.5rem',
    '3xl': '1.875rem',
    '4xl': '2.25rem',
  },
  space: {
    1: '0.25rem',
    2: '0.5rem',
    3: '0.75rem',
    4: '1rem',
    5: '1.25rem',
    6: '1.5rem',
    8: '2rem',
    10: '2.5rem',
    12: '3rem',
    16: '4rem',
  },
  breakpoints: {
    sm: '640px',
    md: '768px',
    lg: '1024px',
    xl: '1280px',
  }
}

export const darkTheme = {
  ...lightTheme,
  colors: {
    ...lightTheme.colors,
    background: '#1a202c',
    surface: '#2d3748',
    text: '#f7fafc',
    textSecondary: '#a0aec0',
    border: '#4a5568',
  }
}

Create components/ThemeProvider/ThemeProvider.js:

import { ThemeProvider as StyledThemeProvider } from 'styled-components'
import { createContext, useContext, useState } from 'react'
import { lightTheme, darkTheme } from '../../styles/theme'

const ThemeContext = createContext()

export function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider')
  }
  return context
}

export default function ThemeProvider({ children }) {
  const [isDark, setIsDark] = useState(false)
  
  const toggleTheme = () => setIsDark(!isDark)
  const theme = isDark ? darkTheme : lightTheme

  return (
    <ThemeContext.Provider value={{ isDark, toggleTheme, theme }}>
      <StyledThemeProvider theme={theme}>
        {children}
      </StyledThemeProvider>
    </ThemeContext.Provider>
  )
}

Responsive Design Patterns

Mobile-First Approach

import styled from 'styled-components'

const ResponsiveGrid = styled.div`
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
  
  /* Tablet */
  @media (min-width: ${props => props.theme.breakpoints.md}) {
    grid-template-columns: repeat(2, 1fr);
    gap: 2rem;
  }
  
  /* Desktop */
  @media (min-width: ${props => props.theme.breakpoints.lg}) {
    grid-template-columns: repeat(3, 1fr);
    gap: 3rem;
  }
  
  /* Large Desktop */
  @media (min-width: ${props => props.theme.breakpoints.xl}) {
    grid-template-columns: repeat(4, 1fr);
  }
`

const ResponsiveText = styled.h1`
  font-size: 1.5rem;
  
  @media (min-width: ${props => props.theme.breakpoints.md}) {
    font-size: 2rem;
  }
  
  @media (min-width: ${props => props.theme.breakpoints.lg}) {
    font-size: 3rem;
  }
`

Container Queries (Modern Approach)

/* CSS Modules approach */
.card {
  container-type: inline-size;
}

@container (min-width: 300px) {
  .cardContent {
    display: flex;
    gap: 1rem;
  }
}

@container (min-width: 500px) {
  .cardContent {
    flex-direction: column;
  }
  
  .cardTitle {
    font-size: 1.5rem;
  }
}

UI Component Libraries Integration

Headless UI with Tailwind

npm install @headlessui/react @heroicons/react

Create components/Modal/Modal.js:

import { Dialog, Transition } from '@headlessui/react'
import { Fragment } from 'react'
import { XMarkIcon } from '@heroicons/react/24/outline'

export default function Modal({ isOpen, onClose, title, children }) {
  return (
    <Transition appear show={isOpen} as={Fragment}>
      <Dialog as="div" className="relative z-50" onClose={onClose}>
        <Transition.Child
          as={Fragment}
          enter="ease-out duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in duration-200"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-black bg-opacity-25" />
        </Transition.Child>

        <div className="fixed inset-0 overflow-y-auto">
          <div className="flex min-h-full items-center justify-center p-4">
            <Transition.Child
              as={Fragment}
              enter="ease-out duration-300"
              enterFrom="opacity-0 scale-95"
              enterTo="opacity-100 scale-100"
              leave="ease-in duration-200"
              leaveFrom="opacity-100 scale-100"
              leaveTo="opacity-0 scale-95"
            >
              <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 shadow-xl transition-all">
                <div className="flex items-center justify-between mb-4">
                  <Dialog.Title className="text-lg font-medium text-gray-900">
                    {title}
                  </Dialog.Title>
                  <button
                    onClick={onClose}
                    className="text-gray-400 hover:text-gray-600"
                  >
                    <XMarkIcon className="h-6 w-6" />
                  </button>
                </div>
                
                {children}
              </Dialog.Panel>
            </Transition.Child>
          </div>
        </div>
      </Dialog>
    </Transition>
  )
}

Framer Motion for Animations

npm install framer-motion

Create components/AnimatedCard/AnimatedCard.js:

import { motion } from 'framer-motion'

const cardVariants = {
  hidden: { opacity: 0, y: 50 },
  visible: { 
    opacity: 1, 
    y: 0,
    transition: { duration: 0.5 }
  },
  hover: { 
    scale: 1.05,
    transition: { duration: 0.2 }
  }
}

export default function AnimatedCard({ children, delay = 0 }) {
  return (
    <motion.div
      variants={cardVariants}
      initial="hidden"
      animate="visible"
      whileHover="hover"
      transition={{ delay }}
      className="bg-white rounded-lg shadow-lg p-6"
    >
      {children}
    </motion.div>
  )
}

Performance Optimization

CSS Optimization

Configure next.config.js for CSS optimization:

const nextConfig = {
  experimental: {
    optimizeCss: true,
  },
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },
}

Critical CSS Loading

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document'

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          {/* Preload critical fonts */}
          <link
            rel="preload"
            href="/fonts/inter-var.woff2"
            as="font"
            type="font/woff2"
            crossOrigin=""
          />
          
          {/* Critical CSS inline */}
          <style jsx>{`
            body {
              font-family: 'Inter', sans-serif;
              margin: 0;
              padding: 0;
            }
          `}</style>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

What's Next?

Excellent progress! You now understand:

  • ✅ CSS Modules for scoped styling
  • ✅ Global CSS and layout patterns
  • ✅ Tailwind CSS integration and customization
  • ✅ Styled Components and theming
  • ✅ Responsive design techniques
  • ✅ UI component libraries
  • ✅ Animation with Framer Motion
  • ✅ Performance optimization

In Part 6 (Final), we'll cover:

  • Deployment strategies (Vercel, Netlify, AWS)
  • Performance optimization and monitoring
  • SEO best practices
  • Testing strategies
  • Production best practices

Practice Exercises

  1. Build a Design System

    • Create reusable components with CSS Modules
    • Implement consistent spacing and typography
    • Add dark mode support
  2. Responsive Dashboard

    • Use Tailwind for responsive grid layouts
    • Implement mobile navigation
    • Add interactive charts
  3. Animated Landing Page

    • Use Framer Motion for page transitions
    • Create scroll-triggered animations
    • Implement smooth hover effects
  4. Component Library

    • Build a set of reusable UI components
    • Document with Storybook
    • Implement theming system

Ready for Part 6? In the final part, we'll learn how to deploy, optimize, and maintain your Next.js applications in production!

Complete our comprehensive Next.js series to become a full-stack Next.js expert.

Share this article: