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 的主题系统是一把双刃剑:用好了是设计系统的基石,用不好是维护的噩梦。
关键要点:
- 统一的设计令牌:在配置文件中集中管理
- CSS 变量桥接:实现运行时主题切换
- 语义化命名:
primary而非blue - 组件化封装:常用样式组合抽取为组件类
记住:Tailwind 不是终点,它是构建设计系统的起点。


