问题场景
你在项目里用 Tailwind CSS 写了一个可复用的 Button 组件,想根据 variant 参数动态切换样式:
tsx
// ❌ 这样写,某些样式死活不生效
function Button({ variant = 'primary' }: { variant: string }) {
return (
<button className={`bg-${variant}-500 hover:bg-${variant}-600 text-white px-4 py-2 rounded`}>
Click Me
</button>
)
}
// 使用
<Button variant="blue" /> // 背景色完全没有...
更诡异的是,如果你把 bg-blue-500 硬编码写死,它就生效。一旦改成字符串拼接,tailwind 就像"选择性失明"。
在控制台查看 DOM,类名 bg-blue-500 明明在元素上,但样式表中根本没有这个类对应的 CSS 规则。
原因分析
Tailwind 的 JIT(Just-In-Time)引擎只做静态分析。
构建时,Tailwind 会扫描你的源码文件,通过正则匹配提取所有看起来像 Tailwind 工具类的字符串,然后只生成这些类的 CSS。
js
// Tailwind 内部的扫描逻辑 ≈ 全局搜索符合 /[a-zA-Z0-9:-]+/ 的模式
// 它看到的是纯文本,不是运行时值
当你写 bg-${variant}-500,扫描器看到的是模板字符串字面量 bg-${variant}-500------它根本无法在构建时解析出 bg-blue-500。所以这个类直接被忽略了,CSS 中根本没有它的定义。
同理,下列写法统统无效:
tsx
// ❌ 动态拼接
<div className={`text-${size}`}>
// ❌ 条件拼接
const cls = 'bg-' + color + '-500'
// ❌ 从对象动态取值
const styles = { primary: 'bg-blue-500', danger: 'bg-red-500' }
<div className={styles[variant]}>
只有完整、硬编码的类名字符串才能被 JIT 引擎识别。
解决方案
方案一:完整类名写入对象(推荐 ✅)
把完整类名作为字符串值存储,而不是让它们在运行时拼接:
tsx
function Button({ variant = 'primary' }: { variant: string }) {
const classes: Record<string, string> = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
danger: 'bg-red-500 hover:bg-red-600 text-white',
success: 'bg-green-500 hover:bg-green-600 text-white',
}
return (
<button className={`px-4 py-2 rounded ${classes[variant] || classes.primary}`}>
Click Me
</button>
)
}
每个变体的类名都是完整字符串,Tailwind 的扫描器能识别每一个。
方案二:safelist 显式声明(适合少量固定值)
在 tailwind.config.js 中告诉 Tailwind:这些类我尽管没直接写,但请生成它们的 CSS:
js
// tailwind.config.js
module.exports = {
safelist: [
'bg-blue-500',
'bg-red-500',
'bg-green-500',
'hover:bg-blue-600',
'hover:bg-red-600',
'hover:bg-green-600',
],
// 也可以使用模式匹配
safelist: [
{ pattern: /^bg-(blue|red|green)-500$/ },
{ pattern: /^hover:bg-(blue|red|green)-600$/ },
],
}
方案三:clsx + twMerge 组合拳
配合 clsx 做条件判断 + tailwind-merge 处理冲突:
tsx
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
function Button({
variant = 'primary',
size = 'md',
disabled = false,
}: ButtonProps) {
return (
<button
className={twMerge(
'px-4 py-2 rounded font-medium transition-colors',
variant === 'primary' && 'bg-blue-500 hover:bg-blue-600 text-white',
variant === 'danger' && 'bg-red-500 hover:bg-red-600 text-white',
size === 'sm' && 'px-2 py-1 text-sm',
size === 'lg' && 'px-6 py-3 text-lg',
disabled && 'opacity-50 cursor-not-allowed'
)}
>
Click Me
</button>
)
}
twMerge 的好处是能智能合并冲突的 Tailwind 类,后声明的覆盖前面的。
要点总结
| 写法 | JIT 能否识别 | 推荐度 |
|---|---|---|
bg-${color}-500 |
❌ 不能 | 永不 |
colors[variant] (字符串拼接) |
❌ 不能 | 永不 |
完整类名字符串 'bg-blue-500 ...' |
✅ 能 | ⭐⭐⭐ |
| safelist 全局声明 | ✅ 能 | ⭐⭐ 适合工具库 |
| clsx/twMerge 条件组合 | ✅ 能 | ⭐⭐⭐ 复杂场景 |
冷知识:为什么直接写 bg-blue-500 可以?
因为 Tailwind 的扫描器就像在代码里做 grep:
bash
# 等价于用正则搜索所有文件
grep -r 'bg-blue-500' src/
如果找到,就生成对应的 CSS。如果找不到------不管运行时拼接出什么花样------CSS 里就没有。
一句话记牢:
Tailwind 的构建时扫描只看源码文本,不执行 JS。
想要动态样式?把完整类名存进对象,别在模板里拼接。
这个坑在迁移老项目到 Tailwind、写组件库时最容易出现,排查时记住:先打开 DevTools 检查样式面板,看类名对应的 CSS 规则是否存在------90% 的"动态类名不生效"都是这个原因。