Tailwind CSS 实战
大约 20 分钟约 5911 字
Tailwind CSS 实战
简介
Tailwind CSS 是原子化 CSS 框架,通过预定义的工具类(Utility Classes)直接在 HTML 中组合样式,无需编写自定义 CSS。相比传统 CSS,Tailwind 避免了命名冲突、样式冗余和维护困难等问题,是现代前端样式方案的热门选择。
Tailwind CSS 的核心设计哲学是 "实用优先"(Utility-First):不提供预制组件(如 Bootstrap 的 .btn、.card),而是提供原子化的低级工具类,由开发者自行组合。这种设计带来以下优势:
- 零抽象成本:样式和结构在同一处,不需要在 CSS 文件和 HTML 之间跳转
- 约束一致性:预定义的设计 token(间距、颜色、字号)天然保证视觉一致
- 无命名负担:不需要思考
.product-card-wrapper-inner这样的类名 - Tree-shaking 友好:未使用的工具类在生产构建中自动移除
特点
基础用法
常用工具类
<!-- 布局 -->
<div class="flex items-center justify-between gap-4">
<span class="flex-1">内容</span>
</div>
<!-- 间距 -->
<div class="p-4 mx-auto mt-8 mb-4">
<!-- p-4 = padding: 1rem -->
<!-- mx-auto = margin-left/right: auto -->
<!-- mt-8 = margin-top: 2rem -->
</div>
<!-- 文字 -->
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">标题</h1>
<p class="text-sm text-gray-500 leading-relaxed">正文内容</p>
<!-- 颜色 -->
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg">
按钮
</button>
<!-- 边框和阴影 -->
<div class="border border-gray-200 rounded-xl shadow-sm p-6">
卡片内容
</div>
<!-- 宽高 -->
<div class="w-full max-w-7xl mx-auto min-h-screen">
容器
</div>间距系统详解
Tailwind 使用 4px 为基数的间距系统,掌握这个规律可以快速写出任何间距:
间距对照表(默认配置):
0 = 0px
0.5 = 2px
1 = 4px ← 基数
1.5 = 6px
2 = 8px
2.5 = 10px
3 = 12px
4 = 16px
5 = 20px
6 = 24px
8 = 32px
10 = 40px
12 = 48px
16 = 64px
20 = 80px
24 = 96px
32 = 128px
40 = 160px
48 = 192px
56 = 224px
64 = 256px
px = 1px<!-- 间距方向助记 -->
<!-- p = padding, m = margin -->
<!-- t = top, b = bottom, l = left, r = right -->
<!-- x = horizontal (left + right) -->
<!-- y = vertical (top + bottom) -->
<div class="p-4"> <!-- 四周 padding: 16px -->
<div class="px-4 py-2"> <!-- 水平 16px, 垂直 8px -->
<div class="pt-4 pb-2"> <!-- 上 16px, 下 8px -->
<div class="mt-auto"> <!-- margin-top: auto(常用于 Flex 推到底部) -->
<div class="-mt-4"> <!-- 负 margin-top: -16px -->
<div class="space-y-4"> <!-- 子元素之间垂直间距 16px(不作用于首项上方和末项下方) -->
<div class="space-x-2"> <!-- 子元素之间水平间距 8px -->颜色系统
<!-- Tailwind 颜色命名规则:{color}-{shade} -->
<!-- shade 从 50(最浅)到 950(最深) -->
<!-- 常用 shade: 50, 100, 200, 300, 400, 500(默认), 600, 700, 800, 900 -->
<!-- 背景色 -->
<div class="bg-blue-500">默认蓝</div>
<div class="bg-blue-100">浅蓝背景</div>
<div class="bg-blue-900">深蓝背景</div>
<!-- 文字色 -->
<span class="text-gray-500">灰色文字</span>
<span class="text-red-600">红色文字</span>
<!-- 边框色 -->
<div class="border border-green-300">绿色边框</div>
<!-- 半透明(使用 /{opacity} 语法) -->
<div class="bg-blue-500/50">50% 透明度的蓝色背景</div>
<div class="bg-black/20">20% 透明度的黑色蒙层</div>
<div class="text-white/80">80% 不透明度的白色文字</div>
<!-- 任意颜色值 -->
<div class="bg-[#1a1a2e]">自定义 hex 颜色</div>
<div class="text-[rgb(59,130,246)]">自定义 rgb 颜色</div>状态修饰符
<!-- 伪类修饰符 -->
<button class="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 focus:ring-2 focus:ring-blue-300">
按钮状态
</button>
<!-- 表单焦点状态 -->
<input class="border border-gray-300 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
<!-- 禁用状态 -->
<button class="bg-gray-300 text-gray-500 cursor-not-allowed" disabled>
禁用按钮
</button>
<!-- 访问过链接 -->
<a class="text-blue-500 hover:text-blue-700 visited:text-purple-500">链接</a>
<!-- 首个子元素 -->
<ul class="space-y-2">
<li class="first:pt-0">第一个项目无上方间距</li>
</ul>
<!-- 最后一个子元素 -->
<div class="divide-y divide-gray-200">
<div class="last:border-b-0">最后一个无底部边框</div>
</div>
<!-- 奇偶项 -->
<tr class="even:bg-gray-50 odd:bg-white">斑马纹行</tr>响应式设计
<!-- 移动优先:默认手机 → sm 平板 → md 桌面 → lg 大屏 -->
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<div class="bg-white rounded-lg p-4">卡片 1</div>
<div class="bg-white rounded-lg p-4">卡片 2</div>
<div class="bg-white rounded-lg p-4">卡片 3</div>
<div class="bg-white rounded-lg p-4">卡片 4</div>
</div>
<!-- 隐藏/显示 -->
<nav class="hidden md:flex gap-6">
<!-- 桌面端显示 -->
<a href="#">首页</a>
<a href="#">关于</a>
</nav>
<button class="md:hidden">
<!-- 移动端显示(汉堡菜单)-->
☰
</button>默认断点配置
断点 最小宽度 CSS 媒体查询
sm 640px @media (min-width: 640px)
md 768px @media (min-width: 768px)
lg 1024px @media (min-width: 1024px)
xl 1280px @media (min-width: 1280px)
2xl 1536px @media (min-width: 1536px)
移动优先策略:不写前缀 = 默认(移动端)样式
添加前缀 = 在该断点及以上生效<!-- 响应式字号 -->
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">响应式标题</h1>
<!-- 响应式布局切换 -->
<div class="flex flex-col md:flex-row gap-4">
<div class="md:w-1/3">侧边栏</div>
<div class="md:w-2/3">主内容</div>
</div>
<!-- 响应式间距 -->
<div class="px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
容器内容
</div>暗色模式
<!-- class 策略:通过在 html 元素添加 dark 类切换 -->
<!-- 在 tailwind.config.ts 中配置 darkMode: 'class' -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<h1 class="text-2xl font-bold">自动适配暗色模式</h1>
<p class="text-gray-500 dark:text-gray-400">描述文字</p>
</div>
<!-- 暗色模式按钮 -->
<button class="bg-blue-500 dark:bg-blue-600 hover:bg-blue-600 dark:hover:bg-blue-700
text-white px-4 py-2 rounded-lg transition-colors">
暗色模式按钮
</button>
<!-- 仅在暗色模式下显示 -->
<span class="hidden dark:inline">🌙 暗色模式已启用</span>
<span class="dark:hidden">☀️ 亮色模式</span>// Vue 3 中实现暗色模式切换
import { ref, watchEffect } from 'vue'
const isDark = ref(false)
function toggleDark() {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
}
// 初始化时读取本地存储
watchEffect(() => {
const saved = localStorage.getItem('theme')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
isDark.value = saved ? saved === 'dark' : prefersDark
document.documentElement.classList.toggle('dark', isDark.value)
})// React 中实现暗色模式切换
import { useState, useEffect } from 'react'
function useDarkMode() {
const [isDark, setIsDark] = useState(false)
useEffect(() => {
const saved = localStorage.getItem('theme')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const shouldBeDark = saved ? saved === 'dark' : prefersDark
setIsDark(shouldBeDark)
document.documentElement.classList.toggle('dark', shouldBeDark)
}, [])
const toggle = () => {
setIsDark(prev => {
const next = !prev
document.documentElement.classList.toggle('dark', next)
localStorage.setItem('theme', next ? 'dark' : 'light')
return next
})
}
return { isDark, toggle }
}组件示例
卡片组件
<!-- 产品卡片 -->
<div class="group bg-white rounded-2xl border border-gray-100 overflow-hidden
hover:shadow-lg transition-shadow duration-300">
<div class="aspect-square bg-gray-100 overflow-hidden">
<img src="product.jpg"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
</div>
<div class="p-5">
<span class="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
新品
</span>
<h3 class="mt-3 text-lg font-semibold text-gray-900">产品名称</h3>
<p class="mt-1 text-sm text-gray-500 line-clamp-2">产品描述文字...</p>
<div class="mt-4 flex items-center justify-between">
<span class="text-xl font-bold text-red-500">¥299</span>
<button class="bg-blue-500 hover:bg-blue-600 text-white text-sm px-4 py-2 rounded-lg
active:scale-95 transition-all">
加入购物车
</button>
</div>
</div>
</div>后台布局
<!-- 后台管理布局 -->
<div class="min-h-screen bg-gray-50">
<!-- 顶部栏 -->
<header class="fixed top-0 left-0 right-0 h-16 bg-white border-b border-gray-200
flex items-center justify-between px-6 z-50">
<h1 class="text-xl font-bold text-gray-800">管理后台</h1>
<div class="flex items-center gap-4">
<span class="text-sm text-gray-500">admin@example.com</span>
<button class="text-sm text-red-500 hover:text-red-600">退出</button>
</div>
</header>
<div class="flex pt-16">
<!-- 侧边栏 -->
<aside class="w-60 fixed left-0 top-16 bottom-0 bg-white border-r border-gray-200 p-4">
<nav class="space-y-1">
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">
📊 仪表盘
</a>
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50">
👥 用户管理
</a>
<a href="#" class="flex items-center gap-3 px-3 py-2 rounded-lg text-gray-600 hover:bg-gray-50">
⚙️ 系统设置
</a>
</nav>
</aside>
<!-- 主内容 -->
<main class="ml-60 flex-1 p-6">
<slot />
</main>
</div>
</div>表单组件
<!-- 完整的表单页面 -->
<form class="max-w-lg mx-auto space-y-6 p-8">
<!-- 文本输入 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
邮箱地址
</label>
<input type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-blue-500
placeholder:text-gray-400 transition-colors"
placeholder="请输入邮箱" />
<p class="mt-1 text-xs text-gray-500">我们不会分享你的邮箱地址</p>
</div>
<!-- 下拉选择 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
角色
</label>
<select class="w-full px-3 py-2 border border-gray-300 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-blue-500
bg-white transition-colors">
<option>管理员</option>
<option>编辑</option>
<option>访客</option>
</select>
</div>
<!-- 错误状态 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
密码
</label>
<input type="password"
class="w-full px-3 py-2 border border-red-500 rounded-lg
focus:ring-2 focus:ring-red-500 focus:border-red-500
text-red-900 placeholder:text-red-300"
value="123" />
<p class="mt-1 text-xs text-red-500">密码长度至少 8 个字符</p>
</div>
<!-- 提交按钮组 -->
<div class="flex items-center gap-3 pt-4">
<button type="submit"
class="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg
hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500
focus:ring-offset-2 transition-colors">
提交
</button>
<button type="button"
class="px-4 py-2 bg-white text-gray-700 text-sm font-medium rounded-lg
border border-gray-300 hover:bg-gray-50
focus:outline-none focus:ring-2 focus:ring-blue-500
focus:ring-offset-2 transition-colors">
取消
</button>
</div>
</form>表格组件
<!-- 数据表格 -->
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
<!-- 表格头部 -->
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">用户列表</h2>
<button class="px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg
hover:bg-blue-700 transition-colors">
新增用户
</button>
</div>
<!-- 表格 -->
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
姓名
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
邮箱
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
角色
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 text-sm text-gray-900">张三</td>
<td class="px-6 py-4 text-sm text-gray-500">zhangsan@example.com</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
bg-green-100 text-green-800">管理员</span>
</td>
<td class="px-6 py-4 text-right text-sm">
<button class="text-blue-600 hover:text-blue-800 mr-3">编辑</button>
<button class="text-red-600 hover:text-red-800">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="px-6 py-3 border-t border-gray-200 flex items-center justify-between">
<span class="text-sm text-gray-500">共 100 条记录</span>
<div class="flex items-center gap-1">
<button class="px-3 py-1 text-sm rounded border border-gray-300 hover:bg-gray-50">
上一页
</button>
<button class="px-3 py-1 text-sm rounded bg-blue-600 text-white">1</button>
<button class="px-3 py-1 text-sm rounded border border-gray-300 hover:bg-gray-50">2</button>
<button class="px-3 py-1 text-sm rounded border border-gray-300 hover:bg-gray-50">3</button>
<button class="px-3 py-1 text-sm rounded border border-gray-300 hover:bg-gray-50">
下一页
</button>
</div>
</div>
</div>加载与空状态
<!-- 骨架屏 -->
<div class="animate-pulse space-y-4">
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
<div class="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
<!-- 卡片骨架屏 -->
<div class="animate-pulse bg-white rounded-xl p-6 space-y-4">
<div class="h-40 bg-gray-200 rounded-lg"></div>
<div class="h-4 bg-gray-200 rounded w-2/3"></div>
<div class="h-3 bg-gray-200 rounded w-full"></div>
<div class="h-3 bg-gray-200 rounded w-4/5"></div>
<div class="flex gap-2">
<div class="h-8 bg-gray-200 rounded w-20"></div>
<div class="h-8 bg-gray-200 rounded w-20"></div>
</div>
</div>
<!-- 空状态 -->
<div class="flex flex-col items-center justify-center py-16 text-gray-400">
<svg class="w-16 h-16 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<p class="text-lg font-medium">暂无数据</p>
<p class="text-sm mt-1">点击上方按钮创建第一条记录</p>
</div>
<!-- Toast 通知 -->
<div class="fixed top-4 right-4 z-50 space-y-2">
<!-- 成功 -->
<div class="flex items-center gap-2 px-4 py-3 bg-green-50 border border-green-200
rounded-lg shadow-lg">
<span class="text-green-600">✓</span>
<span class="text-sm text-green-800">操作成功</span>
</div>
<!-- 错误 -->
<div class="flex items-center gap-2 px-4 py-3 bg-red-50 border border-red-200
rounded-lg shadow-lg">
<span class="text-red-600">✕</span>
<span class="text-sm text-red-800">操作失败,请重试</span>
</div>
</div>主题定制
Tailwind 配置
// tailwind.config.ts
import type { Config } from 'tailwindcss'
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#0078D4',
600: '#005A9E',
700: '#004280'
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif']
},
spacing: {
'18': '4.5rem',
'88': '22rem'
},
borderRadius: {
'4xl': '2rem'
}
}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/line-clamp')
]
} satisfies Config深入主题定制
// tailwind.config.ts —— 完整的企业级配置示例
import type { Config } from 'tailwindcss'
export default {
// 扫描路径:确保覆盖所有模板文件
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
'./src/**/*.{md,mdx}', // 如果使用 MDX
],
// 暗色模式策略:'class' 或 'media'
// class: 手动控制(通过给 html 添加 dark 类)
// media: 跟随系统偏好
darkMode: 'class',
theme: {
// 自定义屏幕断点
screens: {
'xs': '475px', // 小手机
'sm': '640px', // 大手机
'md': '768px', // 平板
'lg': '1024px', // 笔记本
'xl': '1280px', // 桌面
'2xl': '1536px', // 大屏
},
extend: {
// 自定义颜色系统
colors: {
brand: {
50: '#f0f5ff',
100: '#e0ebff',
200: '#c2d6ff',
300: '#94b8ff',
400: '#669aff',
500: '#3b7cff', // 主色
600: '#1a5cef',
700: '#0043cc',
800: '#003399',
900: '#002266',
},
success: {
50: '#f0fdf4',
500: '#22c55e',
600: '#16a34a',
},
warning: {
50: '#fffbeb',
500: '#f59e0b',
600: '#d97706',
},
danger: {
50: '#fef2f2',
500: '#ef4444',
600: '#dc2626',
},
},
// 自定义字体
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
},
// 自定义字号
fontSize: {
'display': ['3.5rem', { lineHeight: '1.1', letterSpacing: '-0.02em' }],
'body-lg': ['1.125rem', { lineHeight: '1.75' }],
},
// 自定义间距
spacing: {
'18': '4.5rem',
'88': '22rem',
'safe-top': 'env(safe-area-inset-top)',
'safe-bottom': 'env(safe-area-inset-bottom)',
},
// 自定义动画
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
'spin-slow': 'spin 3s linear infinite',
'bounce-slow': 'bounce 2s ease-in-out 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' },
},
},
// 自定义阴影
boxShadow: {
'card': '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
'card-hover': '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
'dropdown': '0 10px 15px -3px rgb(0 0 0 / 0.1)',
},
// 自定义圆角
borderRadius: {
'4xl': '2rem',
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/line-clamp'),
],
} satisfies ConfigCSS 变量与 Tailwind 结合
/* 在全局 CSS 中定义 CSS 变量 */
/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--color-primary: 59 124 255;
--color-success: 34 197 94;
--color-warning: 245 158 11;
--color-danger: 239 68 68;
--sidebar-width: 240px;
}
}// tailwind.config.ts —— 引用 CSS 变量
export default {
theme: {
extend: {
colors: {
// 使用 CSS 变量定义颜色(支持运行时动态切换)
primary: 'rgb(var(--color-primary) / <alpha-value>)',
success: 'rgb(var(--color-success) / <alpha-value>)',
},
spacing: {
'sidebar': 'var(--sidebar-width)',
},
},
},
}<!-- 现在可以这样使用 -->
<div class="bg-primary/50 text-primary">使用 CSS 变量的颜色</div>
<div class="ml-sidebar">侧边栏宽度的间距</div>
<!-- 通过 JS 动态切换主题 -->
<script>
document.documentElement.style.setProperty('--color-primary', '220 38 38')
// 所有使用 bg-primary 的元素自动更新为红色
</script>@apply 提取公共样式
/* 当同一组工具类在多处重复出现时,可以提取为组件类 */
/* 使用 @apply 指令 */
@layer components {
/* 按钮基础样式 */
.btn {
@apply inline-flex items-center justify-center px-4 py-2
text-sm font-medium rounded-lg transition-colors
focus:outline-none focus:ring-2 focus:ring-offset-2;
}
.btn-primary {
@apply btn bg-blue-600 text-white hover:bg-blue-700
focus:ring-blue-500;
}
.btn-secondary {
@apply btn bg-white text-gray-700 border border-gray-300
hover:bg-gray-50 focus:ring-blue-500;
}
.btn-danger {
@apply btn bg-red-600 text-white hover:bg-red-700
focus:ring-red-500;
}
/* 表单输入框 */
.form-input {
@apply w-full px-3 py-2 border border-gray-300 rounded-lg
focus:ring-2 focus:ring-blue-500 focus:border-blue-500
placeholder:text-gray-400 transition-colors;
}
.form-input-error {
@apply form-input border-red-500 focus:ring-red-500 focus:border-red-500
text-red-900 placeholder:text-red-300;
}
/* 卡片 */
.card {
@apply bg-white rounded-xl border border-gray-200 p-6
shadow-sm hover:shadow-md transition-shadow;
}
}Tailwind 与 Vue 3 组件封装
<!-- Button.vue -->
<template>
<component
:is="tag"
:class="buttonClasses"
:disabled="disabled"
@click="$emit('click', $event)"
>
<slot />
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
block?: boolean
tag?: string
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
disabled: false,
block: false,
tag: 'button',
})
defineEmits<{
click: [event: MouseEvent]
}>()
const sizeClasses: Record<string, string> = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
}
const variantClasses: Record<string, string> = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 focus:ring-blue-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 focus:ring-gray-500',
}
const buttonClasses = computed(() => [
'inline-flex items-center justify-center font-medium rounded-lg transition-colors',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
sizeClasses[props.size],
variantClasses[props.variant],
props.block ? 'w-full' : '',
props.disabled ? 'opacity-50 cursor-not-allowed' : '',
])
</script>Tailwind 与 React 组件封装
// Badge.tsx
import { type ReactNode } from 'react'
interface BadgeProps {
variant?: 'default' | 'success' | 'warning' | 'danger'
size?: 'sm' | 'md'
children: ReactNode
}
const variantClasses: Record<string, string> = {
default: 'bg-gray-100 text-gray-700',
success: 'bg-green-100 text-green-700',
warning: 'bg-yellow-100 text-yellow-700',
danger: 'bg-red-100 text-red-700',
}
const sizeClasses: Record<string, string> = {
sm: 'px-1.5 py-0.5 text-xs',
md: 'px-2.5 py-1 text-sm',
}
export function Badge({ variant = 'default', size = 'sm', children }: BadgeProps) {
return (
<span className={`
inline-flex items-center font-medium rounded-full
${variantClasses[variant]}
${sizeClasses[size]}
`}>
{children}
</span>
)
}
// 使用示例
function UserStatus() {
return (
<div className="flex items-center gap-2">
<Badge variant="success">在线</Badge>
<Badge variant="warning">离线</Badge>
<Badge variant="danger">异常</Badge>
</div>
)
}性能优化与最佳实践
生产构建优化
// tailwind.config.ts —— 生产优化配置
export default {
content: [
'./src/**/*.{vue,js,ts,jsx,tsx}',
// 确保路径精确,避免扫描不必要的文件
],
// 生产环境不需要 sourcemap
// 在 postcss.config 中配置
}
// postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === 'production' ? {
cssnano: {
preset: 'default',
},
} : {}),
},
}避免常见性能陷阱
<!-- 1. 避免动态拼接类名 —— Tailwind 无法扫描到完整类名 -->
<!-- 错误:Tailwind 无法检测到 bg-blue-500 和 bg-red-500 -->
<div :class="`bg-${color}-500`"></div>
<!-- 正确:使用完整的类名 -->
<div :class="color === 'blue' ? 'bg-blue-500' : 'bg-red-500'"></div>
<!-- 2. 避免在循环中使用动态类名 -->
<!-- 错误 -->
<div v-for="item in items" :key="item.id" :class="`text-${item.color}`"></div>
<!-- 正确:使用映射对象 -->
<script setup>
const colorMap = {
blue: 'text-blue-500',
red: 'text-red-500',
green: 'text-green-500',
}
</script>
<div v-for="item in items" :key="item.id" :class="colorMap[item.color]"></div>
<!-- 3. 避免任意值(方括号语法)过多 —— 增加产物体积 -->
<!-- 少量使用没问题,大量使用应提取到配置中 -->
<div class="w-[calc(100%-2rem)]">偶尔使用没问题</div>
<div class="text-[13px]">如果频繁使用,应配置到 theme.fontSize</div>常用任意值速查
<!-- 任意值语法:[值] -->
<div class="w-[200px]"> <!-- 任意宽度 -->
<div class="top-[117px]"> <!-- 任意位置 -->
<div class="grid-cols-[1fr_2fr_1fr]"><!-- 任意网格 -->
<div class="text-[13px]"> <!-- 任意字号 -->
<div class="bg-[#1a1a2e]"> <!-- 任意颜色 -->
<div class="shadow-[0_0_10px_rgba(0,0,0,0.1)]"> <!-- 任意阴影 -->
<div class="z-[100]"> <!-- 任意 z-index -->
<div class="animate-[bounce_1s_ease-in-out_infinite]"> <!-- 任意动画 -->
<!-- 使用 CSS 函数 -->
<div class="w-[calc(100%-2rem)]"> <!-- calc() -->
<div class="bg-[rgba(0,0,0,0.5)]"> <!-- rgba() -->
<div class="max-w-[min(100%,500px)]"><!-- min() -->与 CSS Modules 对比
| 维度 | Tailwind CSS | CSS Modules |
|---|---|---|
| 命名方式 | 工具类组合 | 本地作用域类名 |
| 样式位置 | HTML 中 | 独立 .module.css 文件 |
| 动态样式 | 条件类名 + 映射对象 | 模板字面量 + 条件 |
| 复用性 | @apply 提取 / 组件封装 | 直接复用类名 |
| 学习成本 | 需要记忆工具类 | 传统 CSS 写法 |
| 产物体积 | 极小(Tree-shaking) | 取决于编写量 |
| IDE 支持 | IntelliSense 插件 | 原生支持 |
优点
缺点
总结
Tailwind CSS 适合追求开发效率的项目。核心用法:工具类组合样式(flex、p-4、text-lg、bg-blue-500)。响应式用 sm/md/lg 前缀。主题定制在 tailwind.config 中扩展。建议在 Vue/React 组件中封装复用样式组合,避免 HTML 过于臃肿。配合 @apply 指令可以提取公共样式。
最佳实践清单:
- 使用组件封装复用样式,而非在模板中重复类名组合
- 动态类名使用映射对象,避免字符串拼接
- 频繁使用的任意值应提取到 tailwind.config
- 使用 CSS 变量实现运行时主题切换
- 配置 content 路径精确,确保 Tree-shaking 生效
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《Tailwind CSS 实战》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《Tailwind CSS 实战》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Tailwind CSS 实战》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Tailwind CSS 实战》最大的收益和代价分别是什么?
