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 sassSCSS 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-componentstypescript
// 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 ButtonTailwind CSS
Installation and Configuration
bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -pjavascript
// 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.css2. 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.