---
title: 一套变量撑起明暗双主题：不用构建链的设计令牌
url: https://vabora.dev/posts/design-tokens-dark-mode
author: Vabora
published: 2026-09-04 09:30:00
updated: 2026-09-26 20:02:28
category: 最佳实践
tags: 架构设计, CSS3, 前端
canonical: https://vabora.dev/posts/design-tokens-dark-mode
license: 转载与引用请注明出处并保留原文链接
---

# 一套变量撑起明暗双主题：不用构建链的设计令牌

> 明暗主题最容易做成"两套颜色到处散落"，改一处漏一处。用 CSS 自定义属性建立令牌层，一个 data-theme 属性切换，含对比度校验与渐变按钮的坑。
## 背景

暗色模式常见两种做法：写两套 CSS（`.dark .card { ... }`），或者用 CSS-in-JS 在运行时算颜色。前者随着组件变多会失控——同一个灰色出现在十几个文件里；后者需要构建链与运行时开销。

官网这类站点有更简单的解法：**把颜色抽成令牌，一处在根上切换**。零构建，纯原生。

## 做法

### 1. 令牌分三层

```css
:root {
    /* 基础色板（只在这里出现真实色值） */
    --x-bg: #f7f8fb;
    --x-fg: #0b1220;
    --x-accent: #2563eb;        /* 科技蓝：白字压上去 5.2:1 */

    /* 派生态（由基础色板推导，组件只用这一层） */
    --x-accent-soft: rgba(37, 99, 235, 0.1);
    --x-line: #e4e8f0;
}

[data-theme='dark'] {
    --x-bg: #05070d;
    --x-fg: #eef3fb;
    --x-accent: #60a5fa;        /* 暗色下提亮，浅蓝底配深墨字 */
}
```

组件里**只允许出现 `var(--x-*)`**，不允许写 `#2563eb`。这条规则靠搜索就能守住：`grep -rn "#[0-9a-f]\{6\}" public/assets/web/css/` 的结果里，只应剩 `:root` 与 `[data-theme]` 两处。

### 2. 切换只有一个属性

```js
document.documentElement.dataset.theme = 'dark'
```

配 `color-scheme: dark` 让浏览器原生控件（滚动条、表单）跟着变——少了这一步，暗色下会留一排刺眼的白色滚动条。

首次绘制要避免闪白：在 `<head>` 里同步读一次 localStorage 并写属性，**不能等 DOMContentLoaded**。

### 3. 对比度必须量出来，不能凭感觉

配色最坑的地方是**渐变按钮上的文字**：浅蓝渐变 + 白字，对比度只有 2:1 左右，看着"很亮"其实读不清。做法是给按钮用**实色底 + 深/浅字**，并写个脚本量一遍：

```bash
php vabora ui:contrast     # 明暗两套主题下逐项检查 WCAG AA
```

检查会覆盖正文、次要文字、按钮、标签、代码块等所有"文字压底色"的组合，低于 4.5:1（大字号 3:1）就报红。这个命令比"设计稿看着还行"可靠得多。

### 4. 少量内联样式的取舍

品牌色渐变文字用 `background-clip: text`，渐变两端都要压得住底色：

```css
.x-hero__title em {
    background: linear-gradient(104deg, var(--x-accent-deep), var(--x-accent));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
```

关键是**两个端点都取深色档**（`--x-accent-deep` → `--x-accent`），否则渐变末端会掉到 3:1 以下。

## 结果

- 明暗切换只改一个属性，无闪烁、无重排；
- 全站 CSS 两个文件（约 76KB，gzip 18KB），没有构建步骤，改完刷新即刻生效；
- 对比度由 `ui:contrast` 把关，改配色时不会悄悄引入不可读的组合。

## 取舍

- **不用 Sass/Tailwind**：站点规模（几十个组件）用原生变量足够；引入构建链的收益是"少打字"，成本是"多一层要维护的工具"。规模上去后可以再换，令牌层能原样迁移。
- **不追动态取色**：从封面图提取主色做主题很酷，但会让同一篇文章在两次访问里长得不一样，也难保证对比度。品牌色固定反而更稳。
