Skip to content

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 sass

SCSS 文件示例

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-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>
  )
}

主题提供者

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 Button

Tailwind CSS

安装和配置

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: [],
}

基础样式

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.css

2. 命名约定

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 - 基础样式和重置样式

选择合适的样式方案取决于项目需求、团队偏好和性能要求。通常建议组合使用多种方案,发挥各自优势。

本站内容仅供学习和研究使用。