返回博客

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:* 类集体哑火。

坑:DevTools 能看到规则,却不代表规则能命中

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:* 工具类都能正确触发。

正解:在 CSS 层桥接,不要在 JS 层补漏

把桥接逻辑写在 globals.css@custom-variant 和 token 块里,激活语义集中在一处。如果改成在 JS 里同时写 .dark 类和 data-theme 属性,则每一条切换路径(设置面板、系统偏好监听、SSR 水合脚本)都要记得双写,极易漏掉。

排查速查

  1. 打开 DevTools,检查 <html> 节点
  2. 若只有 data-theme="dark" 且没有 .dark 类 → 命中此坑
  3. globals.css 里的 @custom-variant dark 确认 matcher 只认 .dark
  4. 按上方两处修改桥接,刷新验证

附: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,就能让数百个工具类集体静默失效。