主题桥接
用 CSS 变量和主题提供方把设计系统主题(亮/暗/品牌)桥接到代码。
为什么
暗模式不再可选——用户期望它,OS 级偏好使它对许多人成默认。主题桥用 CSS 自定义属性(变量)让同组件在任何主题正确渲染,通过换一组变量值而非条件类。这比每元素加 'dark:' 类可维护得多:组件不知主题,只用 var(--surface-bg)。桥还处理品牌主题化(白标 SaaS),同应用服务多品牌不同调色板——用 CSS 变量时这是配置变更非代码变 更。
何时使用
需 ≥2 主题(亮/暗或多品牌)时。约 2-3 小时。单主题内部工具可跳过。
如何做
1. 在 :root 定义语义 token 为 CSS 自定义属性
- --surface-bg、--surface-fg、--border-color、--accent-primary
- 这些是组件唯一引用的颜色——永不原始 hex
2. 在 [data-theme="dark"] 或 .dark 选择器定义主题覆盖
- .dark { --surface-bg: #0a0a0a; --surface-fg: #fafafa; }
- 每主题只是一组变量覆盖,无组件变更
3. 加 ThemeProvider 在 <html> 设 data-theme 并持久化 localStorage
- 首访读 prefers-color-scheme;让用户通过开关覆盖
- 首次绘制前应用主题(<head> 内联脚本)避免 FOUC
4. Tailwind 里把 CSS 变量映射到工具类
- colors: { surface: { bg: "var(--surface-bg)", fg: "var(--surface-fg)" } }
- 则 bg-surface-bg 在所有主题自动工作
5. 多品牌:运行时从配置(DB 或 JSON)加载品牌 token
- 从 JS 在 :root 设 CSS 变量——document.documentElement.style.setProperty('--accent-primary', brand.color)示例
/* globals.css — 语义 token + 暗覆盖 */
:root {
--surface-bg: #ffffff; --surface-fg: #0a0a0a; --accent-primary: #6366f1;
}
.dark {
--surface-bg: #0a0a0a; --surface-fg: #fafafa; --accent-primary: #818cf8;
}
/* tailwind.config.ts */
export default { theme: { extend: { colors: {
surface: { bg: "var(--surface-bg)", fg: "var(--surface-fg)" },
accent: { primary: "var(--accent-primary)" },
} } } };
// ThemeProvider(无 FOUC——<head> 内联脚本)
export function ThemeProvider({ children }) {
useEffect(() => {
const saved = localStorage.getItem("theme") || "system";
const theme = saved === "system" ? matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : saved;
document.documentElement.classList.toggle("dark", theme === "dark");
}, []);
return <>{children}</>;
}常见问题
- ⚠每元素用 Tailwind dark: 变体而非 CSS 变量,加第三主题要碰每组件。
- ⚠加载时无样式内容闪(FOUC)因主题在 React effect 而非内联脚本应用。
- ⚠首访不尊重 OS 偏好,暗模式用户在 effect 跑前得白闪。
- ⚠组件硬编码品牌色而非变量,白标要全 fork 代码库。
最佳实践
- ✓用语义 token 名(surface-bg 非 white)让 token 含义在主题变更中存活。
- ✓React 水合前用 <head> 内联脚本应用主题——完全消除 FOUC。
- ✓Storybook 里每屏在亮和暗模式测——视觉回归抓对比度问题。
- ✓多品牌服务端加载品牌 token 并内联 HTML,无默认品牌闪。