核心就是靠 <html> 上的 dark class 来驱动全局主题切换。机制分三层,都在 apps/app/src/styles.css 里:
- Tailwind 的 dark 变体绑定到 .dark 类
styles.css:3
@custom-variant dark (&:is(.dark *));
这行告诉 Tailwind:所有带 dark: 前缀的类(如 dark:bg-card、dark:text-foreground)只有在某个祖先元素带有 .dark 类时才生效。
- CSS 变量在 .dark 下重新定义
styles.css:190-222
.dark { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); /* ... 其他暗色 token */ }
:root 里定义了一套浅色变量,.dark 块里把同一批 CSS 变量覆盖成深色值。因为变量名一样,所以所有用 var(--background) 的地方会自动换色。
- 基础样式消费这些变量
styles.css:224-235
@layer base { * { border-color: var(--border); outline-color: color-mix(in oklab, var(--ring) 50%, transparent); } body { background-color: var(--background); color: var(--foreground); font-family: var(--font-sans); } }
body 的背景和文字颜色直接来自 CSS 变量,所以只要 .dark 一加,整页底色和文字色就变了。
完整链路
点击 ThemeToggle → useTheme().toggleTheme() ↓ 更新 React state + localStorage ↓ useEffect 在 <html> 上 add/remove 'dark' ↓ .dark 块里的 CSS 变量覆盖 :root 的值 ↓ body / Tailwind dark: 类自动切换颜色
所以 ThemeProvider 里的那个 useEffect 不是在“手动改很多样式”,它只是负责在 <html> 上切一个 class;真正的颜色映射全部由 CSS 变量和 Tailwind 的 dark: 变体接管。
这也是为什么它要挂在 document.documentElement(也就是 <html>)上,而不是 <body> 上——因为 Tailwind 的 @custom-variant dark (&:is(.dark *)) 是从 .dark 祖先开始向下生效的,放在 <html> 上能覆盖整个应用。