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.
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
-
Build a Design System
- Create reusable components with CSS Modules
- Implement consistent spacing and typography
- Add dark mode support
-
Responsive Dashboard
- Use Tailwind for responsive grid layouts
- Implement mobile navigation
- Add interactive charts
-
Animated Landing Page
- Use Framer Motion for page transitions
- Create scroll-triggered animations
- Implement smooth hover effects
-
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.