Next.js 样式处理
Next.js 提供了多种样式解决方案,从传统的 CSS 到现代的 CSS-in-JS。本章将详细介绍各种样式方法及其最佳实践。
样式方案概览
Next.js 支持以下样式方案:
- 全局 CSS - 传统的全局样式表
- CSS Modules - 局部作用域的 CSS
- Sass/SCSS - CSS 预处理器
- CSS-in-JS - styled-components, emotion 等
- Tailwind CSS - 实用优先的 CSS 框架
- PostCSS - CSS 后处理器
全局 CSS
基本使用
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;
}在根布局中引入
typescript
// app/layout.tsx
import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh">
<body>{children}</body>
</html>
)
}CSS Modules
创建 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;
}使用 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>
)
}动态类名组合
typescript
// utils/classNames.ts
export function classNames(...classes: (string | undefined | null | false)[]): string {
return classes.filter(Boolean).join(' ')
}
// 使用示例
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 支持
安装 Sass
bash
npm install --save-dev sassSCSS 文件示例
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;
}
}使用 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>
)
}主题提供者
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>
)
}使用主题
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
安装和配置
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: [],
}基础样式
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 组件
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>
)
}响应式设计
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>
)
}
// 使用示例
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">
加入购物车
</button>
</div>
</div>
))}
</ResponsiveGrid>
)
}动态样式
条件样式
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 变量
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>
)
}性能优化
CSS 代码分割
typescript
// components/LazyComponent.tsx
import dynamic from 'next/dynamic'
// 动态导入组件和样式
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>加载中...</p>,
})
export default function LazyComponent() {
return (
<div>
<h1>主要内容</h1>
<HeavyComponent />
</div>
)
}关键 CSS 内联
typescript
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh">
<head>
<style dangerouslySetInnerHTML={{
__html: `
body { margin: 0; font-family: system-ui; }
.loading { display: flex; justify-content: center; padding: 2rem; }
`
}} />
</head>
<body>{children}</body>
</html>
)
}样式调试
开发工具
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 ? '隐藏边框' : '显示边框'}
</button>
)}
<div className={showOutlines ? 'debug-outlines' : ''}>
{children}
</div>
<style jsx>{`
.debug-outlines * {
outline: 1px solid red !important;
}
`}</style>
</>
)
}最佳实践
1. 样式组织
styles/
├── globals.css # 全局样式
├── variables.css # CSS 变量
├── components/ # 组件样式
│ ├── Button.module.css
│ └── Card.module.css
├── layouts/ # 布局样式
│ └── Header.module.css
└── utilities/ # 工具类
└── spacing.css2. 命名约定
css
/* BEM 命名约定 */
.card { }
.card__header { }
.card__body { }
.card--elevated { }
.card--compact { }
/* CSS Modules */
.container { }
.title { }
.content { }
.isActive { }
.hasError { }3. 性能考虑
typescript
// 避免内联样式
// ❌ 不好
<div style={{ color: 'red', fontSize: '16px' }}>内容</div>
// ✅ 好
<div className={styles.errorText}>内容</div>
// 使用 CSS 变量进行主题切换
// ✅ 好
const theme = {
'--primary-color': isDark ? '#4dabf7' : '#0070f3'
}
<div style={theme}>内容</div>总结
Next.js 提供了丰富的样式解决方案:
- CSS Modules - 适合组件级样式,避免样式冲突
- Tailwind CSS - 快速开发,一致的设计系统
- CSS-in-JS - 动态样式,主题切换
- Sass/SCSS - 复杂样式逻辑,变量和混入
- 全局 CSS - 基础样式和重置样式
选择合适的样式方案取决于项目需求、团队偏好和性能要求。通常建议组合使用多种方案,发挥各自优势。