Tailwind v4 的 dark: 全部哑火——两套暗色系统互不相知
把旧后台的主题切换功能迁移到 Next.js 15 + Tailwind v4 + shadcn/ui 的新栈里时,点击"切换暗色",页面背景确实变黑了,但面包屑文字、顶栏下拉菜单、对话框里的文字全是黑色——黑字压黑底,几乎不可读。
诡异的是:构建零报错,DevTools 里 dark:text-gray-400 产生的 CSS 规则确实存在于样式表中。开关来回切,背景颜色忠实响应,只有前景色纹丝不动。
现象
- 暗色背景:✅ 正常(
body背景变深) dark:text-*/dark:border-*/dark:bg-*/50:❌ 全部无效- DevTools 检查
<html>:只有data-theme="dark"属性,无.dark类 grep -rn "dark:" src/返回数百个命中,但没有一个实际生效
根因
项目里同时存在两套互不知晓的暗色激活系统:
系统 A:shadcn/ui + Tailwind v4
globals.css 由 shadcn CLI 生成,声明了:
@custom-variant dark (&:is(.dark *));
.dark {
--color-background: oklch(0.145 0 0);
--color-foreground: oklch(0.985 0 0);
/* ...全部 token... */
}所有 dark:* 工具类只在 .dark 祖先节点存在时生效。
系统 B:自定义主题切换器(或 next-themes 默认配置) 切换器执行的是:
document.documentElement.setAttribute('data-theme', 'dark')对应旧 CSS 里的 :root[data-theme="dark"] { ... }。这部分背景色正常切换,因此背景"看起来正常"。
两套系统各自独立运行。切换器从不往 <html> 上加 .dark 类,shadcn 的 @custom-variant 永远等不到它,于是数百个 dark:* 类集体哑火。
dark:text-gray-400 会在样式表里生成 :is(.dark *) .dark\:text-gray-400 { color: ... },这条 CSS 是存在的,语法也对。但如果页面上没有任何 .dark 祖先,这条规则永远不会匹配——构建不报错、运行不报错,只是悄悄失效。
正解
在 globals.css 做两处修改,把两条激活路径桥接起来——不需要改任何组件,不需要改切换逻辑。
修改一:shadcn token 块同时响应两种选择器
/* Before */
.dark {
--color-background: oklch(0.145 0 0);
--color-foreground: oklch(0.985 0 0);
}
/* After */
.dark,
:root[data-theme="dark"] {
--color-background: oklch(0.145 0 0);
--color-foreground: oklch(0.985 0 0);
}修改二:@custom-variant 同时匹配两类祖先
/* Before */
@custom-variant dark (&:is(.dark *));
/* After */
@custom-variant dark (&:is(.dark *, [data-theme="dark"] *));这样,无论祖先节点上挂的是 .dark 类还是 data-theme="dark" 属性,所有 dark:* 工具类都能正确触发。
把桥接逻辑写在 globals.css 的 @custom-variant 和 token 块里,激活语义集中在一处。如果改成在 JS 里同时写 .dark 类和 data-theme 属性,则每一条切换路径(设置面板、系统偏好监听、SSR 水合脚本)都要记得双写,极易漏掉。
排查速查
- 打开 DevTools,检查
<html>节点 - 若只有
data-theme="dark"且没有.dark类 → 命中此坑 - 搜
globals.css里的@custom-variant dark确认 matcher 只认.dark - 按上方两处修改桥接,刷新验证
附:next-themes 用户
使用 <ThemeProvider attribute="data-theme"> 是 next-themes 的 SSR 安全默认值,一旦同时安装 shadcn/ui 就必然触发此坑。二选一:
- 应用上面的 CSS 桥接;或
- 改成
<ThemeProvider attribute="class">让 next-themes 写.dark类(shadcn 的预期行为)。
一句话外卖
Tailwind v4 的
@custom-variant是运行时选择器而非编译期检查——dark:*生成的 CSS 规则"存在"不等于"命中",两套暗色激活机制哪怕只差一个 class vs. attribute,就能让数百个工具类集体静默失效。