OKLCH 色板公式
产品中的中性色(页面背景、卡片表面、凹陷区域、分割线、 四级文本)通常每个主题有 15 到 30 个十六进制值,全靠肉眼挑选。 它们会逐渐漂移,暗色模式是另一套手工挑选的集合, 而且没人知道哪些色对仍然满足对比度要求。现在用公式替代它们:
surface-ish token: L = paper + depth × n(token) chroma = tint hue = hue
ink token: L = ink + step × n(token) chroma = ink-tint hue = hue
n(token) 是令牌在阶梯上的位置,每个主题一条阶梯。
只需调整一个输入,整个色阶就会同步移动,因此步骤之间的间隙(即对比度所在之处)
在重新上色后依然保持不变。重新上色只需一行代码(--hue)。
三个控制旋钮(--hue、--lightness、--chroma 或 --contrast)驱动所有颜色。
文件
references/palette.css:可直接使用的规则,包含初始输入和阶梯。scripts/fit_palette.py:将输入拟合到现有的十六进制色板,并打印每个令牌的 ΔE 值。scripts/ladders.json:默认阶梯(亮色、暗色)。scripts/palette-audit.mjs:在真实浏览器中解析每个令牌(两种主题),并在对比度不达标时报错。scripts/oklch.py:转换和 ΔE / WCAG 辅助函数。
步骤
-
按角色而非值来命名令牌。 默认值:
- 背景层:
ground(页面)、panel(侧栏、顶栏)、surface(卡片、菜单)、surface-2(凹陷区域)、surface-3(标签、鼠标悬停行)、field。 - 线条:
line、line-2、line-3(分割线,从最浅到最深),外加field-line(控件边缘,必须满足 3:1 对比度)。 - 文本:
ink、ink-2、ink-3、ink-4(文本,从最深到最浅)。每个文本步骤都是文字内容,因此必须满足 4.5:1 对比度。
- 背景层:
-
拟合,而不是重新挑选。 对于现有产品,运行
python3 scripts/fit_palette.py theme.css。它会读取--color-*的十六进制声明 (先亮色主题,再[data-theme="dark"]规则),将输入与默认阶梯拟合, 并打印每个令牌的 ΔE 值。低于 2 的 ΔE 在视觉上几乎无法区分。 -
在设计对阶梯步骤有不同间距要求时覆盖阶梯步骤,但绝不覆盖颜色本身。 使用
fit_palette.py theme.css light '<inputs>' '<overrides>'评估候选方案, 直到最差的 ΔE 值低于约 2。 常见的覆盖示例:field: 0(字段是卡片表面)、更宽的line-3、ink和ink-2之间更大的间距。 -
编写规则(参见
references/palette.css):- 将输入和
--color-*公式放在承载主题属性的元素上。 - 在暗色规则中重新声明你更改的每一个输入。否则,为工具包的默认值编写的暗色规则 会以它自己的值胜出。
- 在 Tailwind v4 中,在
@theme中保留令牌名称,以便存在相应的工具类; 运行时的声明会覆盖它们。
- 将输入和
-
在真实浏览器中审计:
node scripts/palette-audit.mjs <url>。它会 绘制每个令牌,读取 sRGB 值,并检查:- 每个文本步骤与每个背景层之间是否满足 4.5:1 对比度;
field-line和ring是否满足 3:1 对比度。
修复 FAIL 的方法是移动该令牌的阶梯步骤,而不是对颜色进行特殊处理。 现有色板预期会出现 FAIL:在一次 17 个应用的迁移中,审计 发现其中超过一半的
ink-4或field-line值不达标,而这些值之前都已上线。
陷阱
var()在自定义属性中的解析位置是声明它的地方。- 在
:root上声明公式,并在子元素上设置输入:子元素将永远不会重新着色。 - 别名令牌(
--color-heading: var(--color-ink))也必须在同一个带主题的元素上声明。
- 在
- Tailwind v4 的
@theme值位于:root上。 如果主题限定在应用根节点 ([data-app]),则必须在那里声明公式,而不是在@theme中。 - 任何将颜色传递给 canvas 或地图库的代码
(
getComputedStyle(...).getPropertyValue('--color-x'))现在会收到一个 未解析的oklch(calc(...))字符串。先解析它:绘制一个探测元素 并读取 1px 的画布,方法同审计工具。 - 背景层和文本使用同一个色调。 如果设计的文本色调与背景层不同,
请保持
--ink-tint较低;在亮度 0.98 时,任何色度都会被视为色偏。 - 品牌色、状态色和图表色应保持在公式之外。 文本在品牌色上的变体(例如
brand-text) 应从相同的色调在固定亮度下派生,而不是单独挑选。 - 从设计稿中采样的混合色调、仅亮色主题的色板可能无法拟合到约 2 ΔE 以内。 与其扭曲设计,不如跳过它们。审计仍然适用。