Tailwind CSS 主题系统:构建可扩展的设计令牌体系

HTMLPAGE 团队
13 分钟阅读

深入讲解 Tailwind CSS 的主题配置、CSS 变量集成、暗色模式、动态主题切换等高级技巧,打造灵活可维护的设计系统。

#Tailwind CSS #主题系统 #Design Tokens #CSS变量 #暗色模式

Tailwind CSS 主题系统:构建可扩展的设计令牌体系

Tailwind CSS 不只是一个工具类框架,它更是一个设计令牌系统的基础设施。通过合理的主题配置,你可以构建出既灵活又一致的设计系统。

主题配置基础

扩展 vs 覆盖

// tailwind.config.js
module.exports = {
  theme: {
    // extend: 扩展默认值(推荐)
    extend: {
      colors: {
        brand: '#6366f1', // 添加新颜色,保留默认颜色
      },
    },
    
    // 直接在 theme 下定义会完全覆盖默认值
    // colors: { ... } // 这样会失去所有默认颜色!
  },
};

完整的品牌主题配置

// tailwind.config.js
const colors = require('tailwindcss/colors');

module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx,vue}'],
  
  theme: {
    extend: {
      // 品牌色彩系统
      colors: {
        // 主色
        primary: {
          50: '#eef2ff',
          100: '#e0e7ff',
          200: '#c7d2fe',
          300: '#a5b4fc',
          400: '#818cf8',
          500: '#6366f1',
          600: '#4f46e5',
          700: '#4338ca',
          800: '#3730a3',
          900: '#312e81',
          950: '#1e1b4b',
        },
        // 中性色(覆盖默认 gray)
        gray: {
          50: '#f9fafb',
          100: '#f3f4f6',
          200: '#e5e7eb',
          300: '#d1d5db',
          400: '#9ca3af',
          500: '#6b7280',
          600: '#4b5563',
          700: '#374151',
          800: '#1f2937',
          900: '#111827',
          950: '#030712',
        },
        // 功能色
        success: colors.emerald,
        warning: colors.amber,
        error: colors.red,
        info: colors.sky,
      },
      
      // 字体系统
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
        display: ['Cal Sans', 'Inter', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
      
      // 字号扩展
      fontSize: {
        '2xs': ['0.625rem', { lineHeight: '0.75rem' }],
      },
      
      // 间距扩展
      spacing: {
        '4.5': '1.125rem',
        '18': '4.5rem',
        '112': '28rem',
        '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)',
        'glow': '0 0 20px rgba(99, 102, 241, 0.3)',
        'inner-lg': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.1)',
      },
      
      // 动画
      animation: {
        'fade-in': 'fadeIn 0.5s ease-out',
        'slide-up': 'slideUp 0.5s ease-out',
        'slide-down': 'slideDown 0.3s ease-out',
        'scale-in': 'scaleIn 0.2s ease-out',
        'spin-slow': 'spin 3s linear infinite',
      },
      
      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.95)', opacity: '0' },
          '100%': { transform: 'scale(1)', opacity: '1' },
        },
      },
      
      // 过渡
      transitionDuration: {
        '400': '400ms',
      },
      
      // Z-index
      zIndex: {
        '60': '60',
        '70': '70',
        '80': '80',
        '90': '90',
        '100': '100',
      },
    },
  },
  
  plugins: [],
};

CSS 变量集成

定义语义化变量

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        // 使用 CSS 变量实现动态主题
        background: 'hsl(var(--background))',
        foreground: 'hsl(var(--foreground))',
        
        card: {
          DEFAULT: 'hsl(var(--card))',
          foreground: 'hsl(var(--card-foreground))',
        },
        
        popover: {
          DEFAULT: 'hsl(var(--popover))',
          foreground: 'hsl(var(--popover-foreground))',
        },
        
        primary: {
          DEFAULT: 'hsl(var(--primary))',
          foreground: 'hsl(var(--primary-foreground))',
        },
        
        secondary: {
          DEFAULT: 'hsl(var(--secondary))',
          foreground: 'hsl(var(--secondary-foreground))',
        },
        
        muted: {
          DEFAULT: 'hsl(var(--muted))',
          foreground: 'hsl(var(--muted-foreground))',
        },
        
        accent: {
          DEFAULT: 'hsl(var(--accent))',
          foreground: 'hsl(var(--accent-foreground))',
        },
        
        destructive: {
          DEFAULT: 'hsl(var(--destructive))',
          foreground: 'hsl(var(--destructive-foreground))',
        },
        
        border: 'hsl(var(--border))',
        input: 'hsl(var(--input))',
        ring: 'hsl(var(--ring))',
      },
      
      borderRadius: {
        lg: 'var(--radius)',
        md: 'calc(var(--radius) - 2px)',
        sm: 'calc(var(--radius) - 4px)',
      },
    },
  },
};

定义 CSS 变量

/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    
    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;
    
    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    
    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;
    
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 221.2 83.2% 53.3%;
    
    --radius: 0.5rem;
  }
  
  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    
    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;
    
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 47.4% 11.2%;
    
    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;
    
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    
    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;
    
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;
    
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 224.3 76.3% 48%;
  }
}

暗色模式

基于 class 的暗色模式

// tailwind.config.js
module.exports = {
  darkMode: 'class', // 或 'media' 跟随系统
  // ...
};
<!-- 使用示例 -->
<template>
  <div class="bg-white dark:bg-gray-900">
    <h1 class="text-gray-900 dark:text-white">
      标题
    </h1>
    <p class="text-gray-600 dark:text-gray-400">
      内容文本
    </p>
  </div>
</template>

主题切换实现

<!-- components/ThemeToggle.vue -->
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';

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

const theme = ref<Theme>('system');

function getSystemTheme(): 'light' | 'dark' {
  return window.matchMedia('(prefers-color-scheme: dark)').matches 
    ? 'dark' 
    : 'light';
}

function applyTheme(newTheme: Theme) {
  const root = document.documentElement;
  const isDark = newTheme === 'dark' || 
    (newTheme === 'system' && getSystemTheme() === 'dark');
  
  root.classList.toggle('dark', isDark);
  localStorage.setItem('theme', newTheme);
}

onMounted(() => {
  // 从 localStorage 恢复
  const saved = localStorage.getItem('theme') as Theme | null;
  theme.value = saved || 'system';
  applyTheme(theme.value);
  
  // 监听系统主题变化
  window.matchMedia('(prefers-color-scheme: dark)')
    .addEventListener('change', () => {
      if (theme.value === 'system') {
        applyTheme('system');
      }
    });
});

watch(theme, applyTheme);

function cycleTheme() {
  const themes: Theme[] = ['light', 'dark', 'system'];
  const currentIndex = themes.indexOf(theme.value);
  theme.value = themes[(currentIndex + 1) % themes.length];
}
</script>

<template>
  <button
    @click="cycleTheme"
    class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
  >
    <SunIcon v-if="theme === 'light'" class="w-5 h-5" />
    <MoonIcon v-else-if="theme === 'dark'" class="w-5 h-5" />
    <ComputerIcon v-else class="w-5 h-5" />
  </button>
</template>

多主题支持

品牌主题配置

/* styles/themes.css */
@layer base {
  /* 默认主题 */
  :root {
    --brand-primary: 221.2 83.2% 53.3%;
    --brand-secondary: 262.1 83.3% 57.8%;
  }
  
  /* 主题变体 */
  [data-theme="ocean"] {
    --brand-primary: 199 89% 48%;
    --brand-secondary: 174 72% 56%;
  }
  
  [data-theme="forest"] {
    --brand-primary: 142 71% 45%;
    --brand-secondary: 162 63% 41%;
  }
  
  [data-theme="sunset"] {
    --brand-primary: 24 94% 50%;
    --brand-secondary: 340 82% 52%;
  }
  
  [data-theme="lavender"] {
    --brand-primary: 262 83% 58%;
    --brand-secondary: 292 84% 61%;
  }
}

主题选择器组件

<!-- components/ThemeSelector.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';

const themes = [
  { id: 'default', name: '默认', color: '#6366f1' },
  { id: 'ocean', name: '海洋', color: '#0ea5e9' },
  { id: 'forest', name: '森林', color: '#22c55e' },
  { id: 'sunset', name: '日落', color: '#f97316' },
  { id: 'lavender', name: '薰衣草', color: '#a855f7' },
];

const currentTheme = ref('default');

function setTheme(themeId: string) {
  currentTheme.value = themeId;
  document.documentElement.setAttribute('data-theme', themeId);
  localStorage.setItem('brand-theme', themeId);
}

onMounted(() => {
  const saved = localStorage.getItem('brand-theme');
  if (saved) {
    setTheme(saved);
  }
});
</script>

<template>
  <div class="flex gap-2">
    <button
      v-for="theme in themes"
      :key="theme.id"
      @click="setTheme(theme.id)"
      :class="[
        'w-8 h-8 rounded-full border-2 transition-transform',
        currentTheme === theme.id 
          ? 'border-gray-900 dark:border-white scale-110' 
          : 'border-transparent hover:scale-105'
      ]"
      :style="{ backgroundColor: theme.color }"
      :title="theme.name"
    />
  </div>
</template>

自定义插件

创建工具类插件

// plugins/custom-utilities.js
const plugin = require('tailwindcss/plugin');

module.exports = plugin(function({ addUtilities, theme, e }) {
  // 文字渐变
  addUtilities({
    '.text-gradient': {
      'background': 'linear-gradient(135deg, var(--tw-gradient-from), var(--tw-gradient-to))',
      '-webkit-background-clip': 'text',
      '-webkit-text-fill-color': 'transparent',
      'background-clip': 'text',
    },
  });
  
  // 玻璃态效果
  addUtilities({
    '.glass': {
      'background': 'rgba(255, 255, 255, 0.7)',
      'backdrop-filter': 'blur(10px)',
      '-webkit-backdrop-filter': 'blur(10px)',
      'border': '1px solid rgba(255, 255, 255, 0.2)',
    },
    '.glass-dark': {
      'background': 'rgba(0, 0, 0, 0.5)',
      'backdrop-filter': 'blur(10px)',
      '-webkit-backdrop-filter': 'blur(10px)',
      'border': '1px solid rgba(255, 255, 255, 0.1)',
    },
  });
  
  // 文字阴影
  const textShadows = {
    'text-shadow-sm': '0 1px 2px rgba(0, 0, 0, 0.1)',
    'text-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
    'text-shadow-lg': '0 4px 8px rgba(0, 0, 0, 0.15)',
    'text-shadow-none': 'none',
  };
  
  addUtilities(
    Object.entries(textShadows).reduce((acc, [key, value]) => {
      acc[`.${e(key)}`] = { 'text-shadow': value };
      return acc;
    }, {})
  );
});

创建组件插件

// plugins/components.js
const plugin = require('tailwindcss/plugin');

module.exports = plugin(function({ addComponents, theme }) {
  addComponents({
    // 按钮基础样式
    '.btn': {
      display: 'inline-flex',
      alignItems: 'center',
      justifyContent: 'center',
      padding: `${theme('spacing.2')} ${theme('spacing.4')}`,
      fontSize: theme('fontSize.sm'),
      fontWeight: theme('fontWeight.medium'),
      lineHeight: theme('lineHeight.5'),
      borderRadius: theme('borderRadius.lg'),
      transitionProperty: 'all',
      transitionDuration: '150ms',
      cursor: 'pointer',
      '&:focus': {
        outline: 'none',
        boxShadow: `0 0 0 2px ${theme('colors.white')}, 0 0 0 4px ${theme('colors.primary.500')}`,
      },
      '&:disabled': {
        opacity: '0.5',
        cursor: 'not-allowed',
      },
    },
    
    '.btn-primary': {
      backgroundColor: theme('colors.primary.500'),
      color: theme('colors.white'),
      '&:hover': {
        backgroundColor: theme('colors.primary.600'),
      },
    },
    
    '.btn-secondary': {
      backgroundColor: theme('colors.gray.100'),
      color: theme('colors.gray.900'),
      '&:hover': {
        backgroundColor: theme('colors.gray.200'),
      },
    },
    
    '.btn-outline': {
      backgroundColor: 'transparent',
      borderWidth: '1px',
      borderColor: theme('colors.gray.300'),
      color: theme('colors.gray.700'),
      '&:hover': {
        backgroundColor: theme('colors.gray.50'),
      },
    },
    
    // 卡片样式
    '.card': {
      backgroundColor: theme('colors.white'),
      borderRadius: theme('borderRadius.xl'),
      boxShadow: theme('boxShadow.soft'),
      overflow: 'hidden',
    },
    
    '.card-body': {
      padding: theme('spacing.6'),
    },
    
    '.card-header': {
      padding: theme('spacing.4'),
      borderBottom: `1px solid ${theme('colors.gray.100')}`,
    },
    
    '.card-footer': {
      padding: theme('spacing.4'),
      borderTop: `1px solid ${theme('colors.gray.100')}`,
    },
  });
});

使用插件

// tailwind.config.js
module.exports = {
  // ...
  plugins: [
    require('./plugins/custom-utilities'),
    require('./plugins/components'),
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
    require('@tailwindcss/aspect-ratio'),
  ],
};

响应式设计令牌

自定义断点

// tailwind.config.js
module.exports = {
  theme: {
    screens: {
      'xs': '475px',
      'sm': '640px',
      'md': '768px',
      'lg': '1024px',
      'xl': '1280px',
      '2xl': '1536px',
      // 特殊断点
      'tall': { 'raw': '(min-height: 800px)' },
      'portrait': { 'raw': '(orientation: portrait)' },
      'landscape': { 'raw': '(orientation: landscape)' },
      // 打印样式
      'print': { 'raw': 'print' },
    },
  },
};

容器配置

// tailwind.config.js
module.exports = {
  theme: {
    container: {
      center: true,
      padding: {
        DEFAULT: '1rem',
        sm: '2rem',
        lg: '4rem',
        xl: '5rem',
        '2xl': '6rem',
      },
      screens: {
        sm: '640px',
        md: '768px',
        lg: '1024px',
        xl: '1280px',
        '2xl': '1400px', // 自定义最大宽度
      },
    },
  },
};

与设计工具同步

从 Figma 导出 Tokens

// scripts/sync-tokens.js
const fs = require('fs');

// Figma Tokens 导出的 JSON
const tokens = require('../design-tokens.json');

function generateTailwindConfig(tokens) {
  const config = {
    colors: {},
    spacing: {},
    fontSize: {},
    borderRadius: {},
    boxShadow: {},
  };
  
  // 转换颜色
  if (tokens.colors) {
    Object.entries(tokens.colors).forEach(([key, value]) => {
      if (typeof value === 'string') {
        config.colors[key] = value;
      } else {
        config.colors[key] = {};
        Object.entries(value).forEach(([shade, color]) => {
          config.colors[key][shade] = color.value;
        });
      }
    });
  }
  
  // 转换间距
  if (tokens.spacing) {
    Object.entries(tokens.spacing).forEach(([key, value]) => {
      config.spacing[key] = value.value;
    });
  }
  
  return config;
}

const tailwindTokens = generateTailwindConfig(tokens);

fs.writeFileSync(
  './tailwind.tokens.js',
  `module.exports = ${JSON.stringify(tailwindTokens, null, 2)}`
);

在配置中使用

// tailwind.config.js
const tokens = require('./tailwind.tokens');

module.exports = {
  theme: {
    extend: {
      colors: tokens.colors,
      spacing: tokens.spacing,
      fontSize: tokens.fontSize,
    },
  },
};

性能优化

PurgeCSS 配置

// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,vue,html}',
    './components/**/*.{js,ts,jsx,tsx,vue}',
    // 如果使用第三方组件库
    './node_modules/@your-ui-lib/**/*.{js,ts,jsx,tsx}',
  ],
  
  // 安全列表:始终包含的类
  safelist: [
    'bg-red-500',
    'bg-green-500',
    'bg-blue-500',
    // 动态类名模式
    {
      pattern: /bg-(red|green|blue)-(100|500|900)/,
      variants: ['hover', 'focus'],
    },
    {
      pattern: /text-(xs|sm|base|lg|xl)/,
    },
  ],
};

使用 JIT 模式

// Tailwind CSS v3 默认使用 JIT
// 确保 content 配置正确即可

module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx,vue}'],
  // ...
};

最佳实践

1. 语义化命名

// 推荐:语义化颜色名
colors: {
  primary: { ... },
  secondary: { ... },
  background: 'hsl(var(--background))',
  foreground: 'hsl(var(--foreground))',
}

// 避免:直接使用颜色名
colors: {
  indigo: { ... }, // 换品牌色时需要改很多地方
}

2. 组件化样式

<!-- 使用 @apply 提取组件样式 -->
<style>
.custom-input {
  @apply w-full px-4 py-2 border border-gray-300 rounded-lg;
  @apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
  @apply dark:bg-gray-800 dark:border-gray-600 dark:text-white;
}
</style>

3. 保持一致性

<!-- 使用设计令牌而非硬编码值 -->
<!-- 推荐 -->
<div class="p-4 space-y-4 rounded-lg shadow-soft">

<!-- 避免 -->
<div style="padding: 16px; gap: 16px; border-radius: 8px;">

结语

Tailwind CSS 的主题系统是一把双刃剑:用好了是设计系统的基石,用不好是维护的噩梦。

关键要点:

  1. 统一的设计令牌:在配置文件中集中管理
  2. CSS 变量桥接:实现运行时主题切换
  3. 语义化命名primary 而非 blue
  4. 组件化封装:常用样式组合抽取为组件类

记住:Tailwind 不是终点,它是构建设计系统的起点。