Next.js 样式与 Tailwind

要点

Tailwind 把常用 CSS 规则表达为类名。布局仍受盒模型、可用宽度和继承规则约束;加类名不能替代理解这些规则。

现代样式系统掌握

  • 掌握 Tailwind CSS 4的核心概念和语法
  • 学会构建响应式设计和移动优先布局
  • 理解主题系统和设计令牌的应用
  • 掌握组件样式化和样式复用模式

企业级开发能力

  • 构建可维护的样式架构和组件库
  • 实施暗色模式和多主题支持
  • 掌握样式性能优化和构建策略
  • 学会设计系统和品牌一致性管理

概述

Tailwind CSS 4是功能优先的 CSS 框架,通过原子化类名提供快速、可定制的样式开发体验。与 Next.js 16的深度集成,让现代 Web 应用的样式开发变得高效且一致。

Tailwind CSS 4 项目配置

自动集成配置

# 创建项目时自动集成Tailwind CSS
npx create-next-app@latest my-tailwind-app --typescript --tailwind --eslint --app

1. 或者手动添加Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

tailwind.config.ts 完整配置

// tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
  // 内容源配置
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],

  // 主题配置
  theme: {
    extend: {
      // 颜色系统
      colors: {
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          200: '#bfdbfe',
          300: '#93c5fd',
          400: '#60a5fa',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
          800: '#1e40af',
          900: '#1e3a8a',
          950: '#172554',
        },
        gray: {
          50: '#f9fafb',
          100: '#f3f4f6',
          200: '#e5e7eb',
          300: '#d1d5db',
          400: '#9ca3af',
          500: '#6b7280',
          600: '#4b5563',
          700: '#374151',
          800: '#1f2937',
          900: '#111827',
          950: '#030712',
        },
      },

      // 字体系统
      fontFamily: {
        sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'],
        mono: ['JetBrains Mono', 'ui-monospace', 'monospace'],
        display: ['Cal Sans', 'ui-sans-serif', 'system-ui'],
      },

      // 字号系统
      fontSize: {
        xs: ['0.75rem', { lineHeight: '1rem' }],
        sm: ['0.875rem', { lineHeight: '1.25rem' }],
        base: ['1rem', { lineHeight: '1.5rem' }],
        lg: ['1.125rem', { lineHeight: '1.75rem' }],
        xl: ['1.25rem', { lineHeight: '1.75rem' }],
        '2xl': ['1.5rem', { lineHeight: '2rem' }],
        '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
        '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
        '5xl': ['3rem', { lineHeight: '1' }],
        '6xl': ['3.75rem', { lineHeight: '1' }],
      },

      // 间距系统
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
        '128': '32rem',
      },

      // 圆角系统
      borderRadius: {
        '4xl': '2rem',
      },

      // 阴影系统
      boxShadow: {
        'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
        'medium': '0 4px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
        'hard': '0 10px 40px -10px rgba(0, 0, 0, 0.2), 0 2px 10px -2px rgba(0, 0, 0, 0.04)',
      },

      // 动画系统
      animation: {
        'fade-in': 'fadeIn 0.5s ease-in-out',
        'slide-up': 'slideUp 0.3s ease-out',
        'slide-down': 'slideDown 0.3s ease-out',
        'scale-in': 'scaleIn 0.2s ease-out',
        'bounce-soft': 'bounceSoft 0.6s ease-in-out',
      },

      // 关键帧
      keyframes: {
        fadeIn: {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        slideUp: {
          '0%': { transform: 'translateY(10px)', opacity: '0' },
          '100%': { transform: 'translateY(0)', opacity: '1' },
        },
        slideDown: {
          '0%': { transform: 'translateY(-10px)', opacity: '0' },
          '100%': { transform: 'translateY(0)', opacity: '1' },
        },
        scaleIn: {
          '0%': { transform: 'scale(0.9)', opacity: '0' },
          '100%': { transform: 'scale(1)', opacity: '1' },
        },
        bounceSoft: {
          '0%, 20%, 53%, 80%, 100%': { transform: 'translateY(0)' },
          '40%, 43%': { transform: 'translateY(-8px)' },
          '70%': { transform: 'translateY(-4px)' },
          '90%': { transform: 'translateY(-2px)' },
        },
      },

      // 断点系统
      screens: {
        '3xl': '1600px',
      },
    },
  },

  // 插件配置
  plugins: [
    // 表单插件
    require('@tailwindcss/forms')({
      strategy: 'class',
    }),

    // 排版插件
    require('@tailwindcss/typography'),

    // 容器查询插件
    require('@tailwindcss/container-queries'),

    // 自定义插件
    function({ addUtilities, theme }: any) {
      // 添加自定义工具类
      addUtilities({
        '.text-shadow': {
          textShadow: '0 2px 4px rgba(0,0,0,0.10)',
        },
        '.text-shadow-md': {
          textShadow: '0 4px 8px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.08)',
        },
        '.backface-hidden': {
          backfaceVisibility: 'hidden',
        },
        '.transform-gpu': {
          transform: 'translate3d(0, 0, 0)',
        },
      })

      // 添加渐变文字工具类
      addUtilities({
        '.text-gradient': {
          background: 'linear-gradient(to right, #3b82f6, #8b5cf6)',
          '-webkit-background-clip': 'text',
          '-webkit-text-fill-color': 'transparent',
          'background-clip': 'text',
        },
        '.text-gradient-rainbow': {
          background: 'linear-gradient(to right, #ef4444, #f59e0b, #10b981, #3b82f6, #8b5cf6)',
          '-webkit-background-clip': 'text',
          '-webkit-text-fill-color': 'transparent',
          'background-clip': 'text',
        },
      })
    },
  ],

  // 前缀配置
  prefix: '',

  // 重要标记配置
  important: false,

  // 分离器配置
  separator: ':',

  // 核心插件配置
  corePlugins: {
    preflight: true,
  },

  // 暗色模式配置
  darkMode: ['class'],
}

export default config

基础样式模式

文本样式系统

// src/components/Typography.tsx
import { cn } from '@/lib/utils'
import { cva, type VariantProps } from 'class-variance-authority'

// 标题样式变体
const headingVariants = cva('font-semibold text-gray-900 dark:text-gray-100', {
  variants: {
    size: {
      h1: 'text-4xl md:text-5xl lg:text-6xl',
      h2: 'text-3xl md:text-4xl lg:text-5xl',
      h3: 'text-2xl md:text-3xl lg:text-4xl',
      h4: 'text-xl md:text-2xl lg:text-3xl',
      h5: 'text-lg md:text-xl lg:text-2xl',
      h6: 'text-base md:text-lg lg:text-xl',
    },
    weight: {
      light: 'font-light',
      normal: 'font-normal',
      medium: 'font-medium',
      semibold: 'font-semibold',
      bold: 'font-bold',
    },
    align: {
      left: 'text-left',
      center: 'text-center',
      right: 'text-right',
      justify: 'text-justify',
    },
  },
  defaultVariants: {
    size: 'h2',
    weight: 'semibold',
    align: 'left',
  },
})

// 正文样式变体
const textVariants = cva('text-gray-700 dark:text-gray-300', {
  variants: {
    size: {
      xs: 'text-xs',
      sm: 'text-sm',
      base: 'text-base',
      lg: 'text-lg',
      xl: 'text-xl',
      '2xl': 'text-2xl',
      '3xl': 'text-3xl',
    },
    weight: {
      light: 'font-light',
      normal: 'font-normal',
      medium: 'font-medium',
      semibold: 'font-semibold',
      bold: 'font-bold',
    },
    color: {
      default: 'text-gray-700 dark:text-gray-300',
      muted: 'text-gray-500 dark:text-gray-400',
      primary: 'text-primary-600 dark:text-primary-400',
      secondary: 'text-secondary-600 dark:text-secondary-400',
      accent: 'text-accent-600 dark:text-accent-400',
    },
  },
  defaultVariants: {
    size: 'base',
    weight: 'normal',
    color: 'default',
  },
})

interface HeadingProps extends VariantProps<typeof headingVariants> {
  children: React.ReactNode
  className?: string
}

export function Heading({ children, className, size, weight, align }: HeadingProps) {
  const Tag = size as keyof JSX.IntrinsicElements

  return (
    <Tag className={cn(headingVariants({ size, weight, align }), className)}>
      {children}
    </Tag>
  )
}

interface TextProps extends VariantProps<typeof textVariants> {
  children: React.ReactNode
  className?: string
  as?: keyof JSX.IntrinsicElements
}

export function Text({ children, className, size, weight, color, as: Tag = 'p' }: TextProps) {
  return (
    <Tag className={cn(textVariants({ size, weight, color }), className)}>
      {children}
    </Tag>
  )
}

按钮样式系统

// src/components/Button.tsx
import { forwardRef } from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

// 按钮样式变体
const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
  {
    variants: {
      variant: {
        primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
        secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700',
        outline: 'border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-primary-500 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800',
        ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500 dark:text-gray-300 dark:hover:bg-gray-800',
        danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
        success: 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500',
        warning: 'bg-yellow-500 text-white hover:bg-yellow-600 focus:ring-yellow-500',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4 py-2 text-sm',
        lg: 'h-12 px-6 text-base',
        xl: 'h-14 px-8 text-lg',
      },
      fullWidth: {
        true: 'w-full',
        false: 'w-auto',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
      fullWidth: false,
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  loading?: boolean
  icon?: React.ReactNode
  iconPosition?: 'left' | 'right'
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, fullWidth, loading, icon, iconPosition = 'left', children, disabled, ...props }, ref) => {
    return (
      <button
        className={cn(buttonVariants({ variant, size, fullWidth }), className)}
        ref={ref}
        disabled={disabled || loading}
        {...props}
      >
        {loading && (
          <svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
            <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
            <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
          </svg>
        )}

        {icon && iconPosition === 'left' && !loading && (
          <span className="mr-2">{icon}</span>
        )}

        {children}

        {icon && iconPosition === 'right' && (
          <span className="ml-2">{icon}</span>
        )}
      </button>
    )
  }
)

Button.displayName = 'Button'

卡片组件样式

// src/components/Card.tsx
import { forwardRef } from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

// 卡片样式变体
const cardVariants = cva('rounded-xl border bg-white shadow-soft dark:bg-gray-900 dark:border-gray-800', {
  variants: {
    variant: {
      default: 'border-gray-200 dark:border-gray-800',
      elevated: 'border-gray-100 shadow-medium dark:border-gray-700',
      outlined: 'border-2 border-gray-300 shadow-none dark:border-gray-600',
      ghost: 'border-transparent shadow-none bg-transparent',
    },
    padding: {
      none: 'p-0',
      sm: 'p-4',
      md: 'p-6',
      lg: 'p-8',
      xl: 'p-10',
    },
    hover: {
      true: 'transition-all duration-200 hover:shadow-medium hover:-translate-y-1',
      false: '',
    },
  },
  defaultVariants: {
    variant: 'default',
    padding: 'md',
    hover: false,
  },
})

export interface CardProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof cardVariants> {}

export const Card = forwardRef<HTMLDivElement, CardProps>(
  ({ className, variant, padding, hover, ...props }, ref) => (
    <div
      ref={ref}
      className={cn(cardVariants({ variant, padding, hover }), className)}
      {...props}
    />
  )
)

Card.displayName = 'Card'

// 卡片头部
export const CardHeader = forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn('flex flex-col space-y-1.5 p-6', className)}
    {...props}
  />
))

CardHeader.displayName = 'CardHeader'

// 卡片标题
export const CardTitle = forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h3
    ref={ref}
    className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
    {...props}
  />
))

CardTitle.displayName = 'CardTitle'

// 卡片描述
export const CardDescription = forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <p
    ref={ref}
    className={cn('text-sm text-gray-500 dark:text-gray-400', className)}
    {...props}
  />
))

CardDescription.displayName = 'CardDescription'

// 卡片内容
export const CardContent = forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
))

CardContent.displayName = 'CardContent'

// 卡片底部
export const CardFooter = forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn('flex items-center p-6 pt-0', className)}
    {...props}
  />
))

CardFooter.displayName = 'CardFooter'

响应式设计系统

移动优先布局

// src/components/ResponsiveGrid.tsx
import { cn } from '@/lib/utils'

interface ResponsiveGridProps {
  children: React.ReactNode
  className?: string
  cols?: {
    sm?: number
    md?: number
    lg?: number
    xl?: number
    '2xl'?: number
  }
  gap?: {
    sm?: number
    md?: number
    lg?: number
    xl?: number
  }
}

export function ResponsiveGrid({
  children,
  className,
  cols = { sm: 1, md: 2, lg: 3, xl: 4 },
  gap = { sm: 4, md: 6, lg: 8 }
}: ResponsiveGridProps) {
  const gridClasses = cn(
    'grid',
    cols.sm && `grid-cols-${cols.sm}`,
    cols.md && `md:grid-cols-${cols.md}`,
    cols.lg && `lg:grid-cols-${cols.lg}`,
    cols.xl && `xl:grid-cols-${cols.xl}`,
    gap.sm && `gap-${gap.sm}`,
    gap.md && `md:gap-${gap.md}`,
    gap.lg && `lg:gap-${gap.lg}`,
    gap.xl && `xl:gap-${gap.xl}`,
    className
  )

  return <div className={gridClasses}>{children}</div>
}

// 使用示例
export function ProductGrid() {
  return (
    <ResponsiveGrid
      cols={{ sm: 1, md: 2, lg: 3, xl: 4 }}
      gap={{ sm: 4, md: 6, lg: 8 }}
      className="py-8"
    >
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </ResponsiveGrid>
  )
}

响应式容器

// src/components/Container.tsx
import { cn } from '@/lib/utils'

interface ContainerProps {
  children: React.ReactNode
  className?: string
  size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
  centered?: boolean
}

export function Container({
  children,
  className,
  size = 'lg',
  centered = true
}: ContainerProps) {
  const sizeClasses = {
    sm: 'max-w-sm',
    md: 'max-w-md',
    lg: 'max-w-7xl',
    xl: 'max-w-screen-xl',
    full: 'max-w-full'
  }

  return (
    <div
      className={cn(
        'w-full px-4 sm:px-6 lg:px-8',
        sizeClasses[size],
        centered && 'mx-auto',
        className
      )}
    >
      {children}
    </div>
  )
}

响应式图片

// src/components/ResponsiveImage.tsx
import Image from 'next/image'
import { useState } from 'react'

interface ResponsiveImageProps {
  src: string
  alt: string
  width: number
  height: number
  priority?: boolean
  className?: string
  sizes?: string
  quality?: number
  fill?: boolean
}

export function ResponsiveImage({
  src,
  alt,
  width,
  height,
  priority = false,
  className = '',
  sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw',
  quality = 75,
  fill = false
}: ResponsiveImageProps) {
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState(false)

  if (error) {
    return (
      <div className={cn(
        'flex items-center justify-center bg-gray-200 dark:bg-gray-800 text-gray-500',
        fill ? 'absolute inset-0' : 'w-full h-full',
        className
      )}>
        <span className="text-sm">图片加载失败</span>
      </div>
    )
  }

  return (
    <div className={cn('relative overflow-hidden', className)}>
      <Image
        src={src}
        alt={alt}
        width={fill ? undefined : width}
        height={fill ? undefined : height}
        fill={fill}
        priority={priority}
        quality={quality}
        sizes={sizes}
        className={cn(
          'transition-opacity duration-300',
          isLoading ? 'opacity-0' : 'opacity-100',
          fill ? 'object-cover' : 'w-full h-auto'
        )}
        onLoadingComplete={() => setIsLoading(false)}
        onError={() => setError(true)}
      />

      {isLoading && (
        <div className="absolute inset-0 bg-gray-200 dark:bg-gray-800 animate-pulse" />
      )}
    </div>
  )
}

主题系统实现

暗色模式配置

// src/components/ThemeProvider.tsx
'use client'

import { createContext, useContext, useEffect, useState } from 'react'

type Theme = 'light' | 'dark' | 'system'

interface ThemeContextType {
  theme: Theme
  setTheme: (theme: Theme) => void
  systemTheme: 'light' | 'dark'
  effectiveTheme: 'light' | 'dark'
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

export function ThemeProvider({
  children,
  defaultTheme = 'system',
  storageKey = 'ui-theme',
}: {
  children: React.ReactNode
  defaultTheme?: Theme
  storageKey?: string
}) {
  const [theme, setTheme] = useState<Theme>(defaultTheme)
  const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>('light')

  // 获取系统主题
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    setSystemTheme(mediaQuery.matches ? 'dark' : 'light')

    const handleChange = (e: MediaQueryListEvent) => {
      setSystemTheme(e.matches ? 'dark' : 'light')
    }

    mediaQuery.addEventListener('change', handleChange)
    return () => mediaQuery.removeEventListener('change', handleChange)
  }, [])

  // 从localStorage读取主题设置
  useEffect(() => {
    const stored = localStorage.getItem(storageKey) as Theme
    if (stored) {
      setTheme(stored)
    }
  }, [storageKey])

  // 应用主题到HTML元素
  useEffect(() => {
    const root = window.document.documentElement
    root.classList.remove('light', 'dark')

    let effectiveTheme: 'light' | 'dark'
    if (theme === 'system') {
      effectiveTheme = systemTheme
    } else {
      effectiveTheme = theme
    }

    root.classList.add(effectiveTheme)
  }, [theme, systemTheme])

  const value = {
    theme,
    setTheme: (theme: Theme) => {
      localStorage.setItem(storageKey, theme)
      setTheme(theme)
    },
    systemTheme,
    effectiveTheme: theme === 'system' ? systemTheme : theme,
  }

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

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

主题切换组件

// src/components/ThemeToggle.tsx
'use client'

import { Moon, Sun, Monitor } from 'lucide-react'
import { useTheme } from './ThemeProvider'
import { Button } from './Button'

export function ThemeToggle() {
  const { theme, setTheme, effectiveTheme } = useTheme()

  const themes = [
    { value: 'light' as const, label: '浅色', icon: Sun },
    { value: 'dark' as const, label: '深色', icon: Moon },
    { value: 'system' as const, label: '跟随系统', icon: Monitor },
  ]

  return (
    <div className="flex items-center space-x-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
      {themes.map(({ value, label, icon: Icon }) => (
        <Button
          key={value}
          variant={theme === value ? 'primary' : 'ghost'}
          size="sm"
          onClick={() => setTheme(value)}
          className="h-8 px-2"
          title={label}
        >
          <Icon className="h-4 w-4" />
        </Button>
      ))}
    </div>
  )
}

高级样式模式

渐变和图案

// src/components/GradientBackground.tsx
import { cn } from '@/lib/utils'

interface GradientBackgroundProps {
  children: React.ReactNode
  className?: string
  variant?: 'linear' | 'radial' | 'conic'
  colors: string[]
  direction?: string
  animated?: boolean
}

export function GradientBackground({
  children,
  className,
  variant = 'linear',
  colors,
  direction = 'to right',
  animated = false
}: GradientBackgroundProps) {
  const gradientClass = cn(
    'absolute inset-0',
    variant === 'linear' && `bg-gradient-${direction}`,
    variant === 'radial' && 'bg-radial-gradient',
    variant === 'conic' && 'bg-conic-gradient',
    animated && 'animate-gradient',
    className
  )

  const gradientStyle = {
    ...(variant === 'linear' && {
      backgroundImage: `linear-gradient(${direction}, ${colors.join(', ')})`
    }),
    ...(variant === 'radial' && {
      backgroundImage: `radial-gradient(circle, ${colors.join(', ')})`
    }),
    ...(variant === 'conic' && {
      backgroundImage: `conic-gradient(from 0deg, ${colors.join(', ')})`
    }),
  }

  return (
    <div className="relative overflow-hidden">
      <div
        className={gradientClass}
        style={gradientStyle}
      />
      <div className="relative z-10">
        {children}
      </div>
    </div>
  )
}

// 使用示例
export function HeroSection() {
  return (
    <GradientBackground
      colors={['#3b82f6', '#8b5cf6', '#ec4899']}
      direction="to bottom right"
      animated
    >
      <div className="py-20 text-center text-white">
        <h1 className="text-5xl font-bold mb-6">欢迎来到未来</h1>
        <p className="text-xl opacity-90">体验现代化的Web开发</p>
      </div>
    </GradientBackground>
  )
}

玻璃态效果

// src/components/GlassEffect.tsx
import { cn } from '@/lib/utils'

interface GlassEffectProps {
  children: React.ReactNode
  className?: string
  blur?: 'sm' | 'md' | 'lg' | 'xl'
  opacity?: 'low' | 'medium' | 'high'
  border?: boolean
}

export function GlassEffect({
  children,
  className,
  blur = 'md',
  opacity = 'medium',
  border = true
}: GlassEffectProps) {
  const blurClasses = {
    sm: 'backdrop-blur-sm',
    md: 'backdrop-blur-md',
    lg: 'backdrop-blur-lg',
    xl: 'backdrop-blur-xl'
  }

  const opacityClasses = {
    low: 'bg-white/10 dark:bg-black/10',
    medium: 'bg-white/20 dark:bg-black/20',
    high: 'bg-white/30 dark:bg-black/30'
  }

  return (
    <div
      className={cn(
        'relative',
        blurClasses[blur],
        opacityClasses[opacity],
        border && 'border border-white/20 dark:border-white/10',
        'rounded-xl',
        className
      )}
    >
      {children}
    </div>
  )
}

性能优化策略

样式分离和关键 CSS

// src/components/CriticalStyles.tsx
import Head from 'next/head'

export function CriticalStyles() {
  const criticalCSS = `
    /* 关键路径样式 */
    body {
      margin: 0;
      padding: 0;
      font-family: 'Inter', system-ui, sans-serif;
      line-height: 1.6;
    }

    .loading-skeleton {
      background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
      background-size: 200% 100%;
      animation: loading 1.5s infinite;
    }

    @keyframes loading {
      0% { background-position: 200% 0; }
      100% { background-position: -200% 0; }
    }

    /* 隐藏未加载的图片 */
    img:not([src]) {
      visibility: hidden;
    }
  `

  return (
    <Head>
      <style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
    </Head>
  )
}

PurgeCSS 配置优化

// tailwind.config.ts (生产环境优化)
const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],

  // 生产环境优化
  ...(process.env.NODE_ENV === 'production' && {
    purge: {
      enabled: true,
      content: [
        './src/**/*.{js,ts,jsx,tsx}',
        './src/pages/**/*.{html}',
      ],
      options: {
        safelist: [
          // 动态生成的类名
          /^bg-/,
          /^text-/,
          /^border-/,
          /^hover:/,
          /^focus:/,
          /^dark:/,
        ],
      },
    },
  }),
}

总结

通过本教程,你已经掌握了:

  1. 配置系统: Tailwind CSS 4的完整配置和定制
  2. 组件样式: 可复用的样式组件和变体系统
  3. 响应式设计: 移动优先的响应式布局策略
  4. 主题系统: 暗色模式和多主题支持
  5. 高级效果: 渐变、玻璃态等现代视觉效果
  6. 性能优化: 样式分离和构建优化策略

下一步学习

  • 深入学习设计系统和设计令牌
  • 掌握 CSS-in-JS 和 Tailwind 的混合使用
  • 学习动画和交互效果的最佳实践
  • 探索无障碍设计和可访问性
  • 了解样式系统的测试和维护

样式工具只负责表达设计决策;可读性、可访问性、主题切换和构建体积仍需要在真实页面和实际交互中验证。

文档交叉引用

相关文档

参考章节

  • 📖 [本模块其他章节]: 布局路由设计 | 数据获取基础
  • 📖 [Knowledge Points 快速参考]: CSS 模式参考 | 样式工具配置

总结

样式系统的验收边界

  1. 令牌表达语义:颜色、间距和圆角等常用值以用途命名,例如“危险操作”而非“红色 500”;组件变体从这些令牌组合,避免相似页面逐处复制不一致的类串。
  2. 响应式从内容开始:先在最窄可用宽度完成阅读、输入和操作,再为内容确实拥挤的断点调整布局。只拖动浏览器并看到元素没有溢出,不足以证明触控和文字缩放可用。
  3. 主题切换不闪烁也不丢语义:初始化主题前避免错误主题短暂显示;前景/背景、边框、焦点和状态色都要在每个主题中保持足够对比,不能只切换页面背景。
  4. 变体输入受约束:组件公开有限的 size、tone、loading 等变体;未知输入回退到安全样式或由类型与运行时校验拒绝,不能让任意类名绕过设计系统。
  5. 性能从产物和体验测量:检查生产构建的 CSS、关键页面加载与交互;不要仅根据“使用了 utility class”推断 CSS 更小或页面更快。

学习成果验收

  • [ ] 为按钮、提示和表单错误定义语义令牌;更改一个令牌后,确认相关组件一致更新且无无关页面受影响。
  • [ ] 在窄屏、宽屏、文字放大和键盘导航下完成关键表单,记录溢出、焦点与可读性结果。
  • [ ] 切换浅色和深色主题并刷新页面,检查没有明显主题闪烁,错误、禁用、焦点和正文对比都可辨认。
  • [ ] 传入一个未定义变体,确认类型检查或运行时回退能阻止不可预测的样式。
  • [ ] 比较生产构建前后关键页面的 CSS 产物和用户可见加载结果,记录环境与测量条件。

贡献与反馈

内容改进

如果你发现本文档有改进空间,欢迎: – 🐛 报告问题: 在 Issues 中提出具体问题 – 💡 建议改进: 提出修改建议和补充内容 – 📝 参与贡献: 提交 PR 完善文档内容

学习反馈

分享你的学习体验: – ✅ 有用内容: 哪些部分对你最有帮助 – ❓ 疑问点: 哪些内容需要进一步澄清 – 🎯 建议: 希望增加什么内容


文档状态: ✅ 已完成最后更新: 2026年9月版本: v1.0.0

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇