色彩
颜色分三层:基础色板(primitive,只声明一次的原始值)、语义角色(semantic,bg-* / fg-* / border-* / ring-*,皮肤只消费这一层)、组件覆盖槽(--xh-<组件>-*)。这一页讲前两层怎么定,以及它们之间的对比度判据。产物与引入方式见设计令牌与主题。
色彩模型
所有颜色写成 oklch(L C H),不用 hex / hsl。OKLCH 的明度 L 是感知均匀的:同一 L 的不同色相看起来一样亮,对比度只由 L 决定。于是整套色板可以只定一条明度曲线,换色相不改对比度;深色主题不是逐色手调,而是沿同一条曲线反向取档。
基础色板
十二个色相,每个色相 11 档(50 – 950)。每档的明度与彩度取品牌曲线的基线,彩度再按该档明度与色相收进 sRGB 色域。色相 258 那一族(indigo)与品牌色逐值相同——品牌就是色板里的一员。
怎么读这张色板:
- 同一档跨色相同一明度。 600 档 L 0.546,任何色相的 600 档上铺白字都过 3:1(大字与图形档),700 档上铺白字都过 4.5:1;50 – 200 档上铺
--xh-fg-default正文超过 9:1,100 档上铺同色相的 700 档文字过 4.5:1。给数据图、标签、头像按颜色点名时,换色相不必重算对比度。 - 600 是锚点。 实心底取 600(配白字的正文档取 700),悬停 700、按下 800;淡底取 100 / 200,淡底上的文字取 700。
- 黄、青这类天然明亮的色相中档偏沉。 这是"同档同明度"的代价:要一块亮黄,取 200 / 300 档,而不是把 600 调亮。
- 皮肤不直接消费色板。 组件皮肤只认语义角色与语气轴;色板给使用者、数据可视化与自定义语气用。
色板由 packages/design/tokens/build/emit-palette.mjs 从 tokens/palette.seeds.json 的十二个色相角派生,改色相只改种子;tests/palette.spec.ts 逐档核对生成物、明度与运行时 deriveBrandScale 同源。
中性色板
十六档,含 450 / 550 / 650 / 750 四个半档:中性色承担文字、背景、边界三种角色,正文与次要文字、装饰边与控件边、面与抬起的面之间常常只差半档,整档跳会太跳。
| 档 | 浅色主题里的角色 | 深色主题里的角色 |
|---|---|---|
| 0 / 50 | 面(--xh-bg-surface)与页面底(--xh-bg-page) | 反白前景 |
| 100 – 300 | 淡底与三档边线(subtle 100 / default 200 / strong 300) | — |
| 400 / 550 / 600 | 禁用字 / 次要字 / 正文次级 | — |
| 700 / 800 | — | 装饰边(default 700)与淡底 |
| 900 / 950 | 正文(950) | 面(900)与页面底(950) |
品牌色
品牌色不是一枚颜色,是一条从种子派生出来的 11 档梯度。派生只取种子的色相与彩度,明度曲线原样保留——语义层与语气层建立在明度之上的对比度保证,对任何种子都成立。
import { registerBrand } from "@xihan-ui/tokens/runtime";
// 种子锚定在 600 档,即实心底与强调文字用的那一档
const dispose = registerBrand("ocean", "oklch(0.546 0.16 215)");
document.documentElement.dataset.brand = "ocean";registerBrand 把 [data-brand='ocean'] 取值块注进文档;不透明的任何 CSS 颜色都能当种子,落在 sRGB 色域外的彩度逐档收进来。切换品牌只换 data-brand,品牌淡底(12% / 20% / 28% 拼色)、焦点环、指示条全部跟着走。运行时 API 见 皮肤与样式分层 · 切换品牌色。
功能色
四种语气各有一族原语,档位按"每一档要落在哪块面上"逐档验过对比度,不与基础色板共用曲线:黄族到 700 档也只有 3.75:1,够不着高对比档的判据,因此 warning 的 600 以上把色相从 86 转到 70。
| 语气 | 原语 | 与基础色板的关系 | 典型用途 |
|---|---|---|---|
| danger | --xh-color-danger-400 … 700 | 色相 25,同 red | 破坏性动作、校验失败、错误状态 |
| success | --xh-color-success-500 … 700 | 色相 149,同 green | 完成、通过、在线 |
| warning | --xh-color-warning-400 … 800 | 色相 86 → 70,介于 amber 与 yellow | 需要注意但不阻断 |
| info | --xh-color-info-500 … 700 | 色相 237,同 blue | 中性提示、进行中 |
danger 动作与 error 状态分开定义,不共用业务语义;状态色表达任务结果,不表达空间层级。
语义角色
皮肤只消费这一层。每个实色底都配了前景(--xh-fg-on-brand、--xh-tone-on),组件不自行计算文字色。取值随 data-theme 翻转,下面的色块画的是当前主题。
背景
| 令牌 | 取值 | 预览 | 说明 |
|---|---|---|---|
--xh-bg-page | var(--xh-color-neutral-50) | 页面底:面之下那一层,铺满视口 | |
--xh-bg-canvas | var(--xh-color-neutral-0) | 不透明画布:自动填充遮罩、色块选中环等必须不透明的地方;控件盒静息不再填它 | |
--xh-bg-surface | var(--xh-color-neutral-0) | 静态内容面缺省底 | |
--xh-bg-surface-raised | var(--xh-color-neutral-0) | 抬起的面:Card、滑块 | |
--xh-bg-subtle | var(--xh-color-neutral-100) | 淡底;白底上的 hover | |
--xh-bg-subtle-hover | var(--xh-color-neutral-200) | 白底上的 pressed;淡底上的 hover | |
--xh-bg-subtle-active | var(--xh-color-neutral-300) | 淡底上的 pressed,只留给按下 | |
--xh-bg-muted | var(--xh-color-neutral-100) | 禁用实心钮退到的中性面 | |
--xh-bg-brand | var(--xh-color-brand-600) | 主要动作实心底 | |
--xh-bg-brand-hover | var(--xh-color-brand-700) | ||
--xh-bg-brand-active | var(--xh-color-brand-800) | ||
--xh-bg-brand-subtle | color-mix(in oklab, var(--xh-bg-brand) 12%, var(--xh-bg-surface)) | 选中 / 当前专属,12% 品牌拼色 | |
--xh-bg-brand-subtle-hover | color-mix(in oklab, var(--xh-bg-brand) 20%, var(--xh-bg-surface)) | ||
--xh-bg-brand-subtle-active | color-mix(in oklab, var(--xh-bg-brand) 28%, var(--xh-bg-surface)) | ||
--xh-bg-overlay | oklch(0 0 0 / 0.45) | 模态遮罩 |
前景
| 令牌 | 取值 | 预览 | 说明 |
|---|---|---|---|
--xh-fg-default | var(--xh-color-neutral-950) | 正文 | |
--xh-fg-muted | var(--xh-color-neutral-600) | 次要文字、说明 | |
--xh-fg-subtle | var(--xh-color-neutral-550) | 占位、禁用标签 | |
--xh-fg-disabled | var(--xh-color-neutral-400) | 禁用文字,对画布不低于 2.5:1 | |
--xh-fg-on-brand | var(--xh-color-neutral-0) | 实心品牌底上的字 | |
--xh-fg-on-brand-subtle | color-mix(in oklab, var(--xh-bg-brand) 60%, var(--xh-fg-default)) | 品牌淡底上的字 | |
--xh-fg-brand | var(--xh-color-brand-600) | 普通底上的品牌文字与对号 | |
--xh-fg-brand-strong | var(--xh-color-brand-700) | ||
--xh-fg-success | var(--xh-color-success-700) | ||
--xh-fg-warning | var(--xh-color-warning-700) | ||
--xh-fg-info | var(--xh-color-info-700) | ||
--xh-fg-danger | var(--xh-color-danger-600) |
边界与焦点环
| 令牌 | 取值 | 预览 | 说明 |
|---|---|---|---|
--xh-border-default | var(--xh-color-neutral-200) | 一切根面外边与 raised 面描边 | |
--xh-border-subtle | var(--xh-color-neutral-100) | 只作内部分隔线 | |
--xh-border-strong | var(--xh-color-neutral-300) | 只作高对比档与刻意登记的强调边 | |
--xh-border-control | var(--xh-border-default) | 控件边界,缺省档与 border-default 同色;高对比档才加深到 3:1 | |
--xh-border-control-hover | var(--xh-color-neutral-400) | 控件悬停边 | |
--xh-border-control-focus | var(--xh-ring-focus) | 聚焦边,与焦点环同色、不随语气 | |
--xh-border-invalid | var(--xh-color-danger-600) | ||
--xh-ring-focus | var(--xh-color-brand-600) | 公共键盘焦点环,对画布、面与淡底都 ≥ 3:1 | |
--xh-ring-invalid | var(--xh-color-danger-500) |
语气轴
data-tone 是六族语气的切换轴:brand、neutral、danger、warning、success、info。写在任何节点上,节点内即可取到整族 --xh-tone-*:实心底、实心底上的前景、淡底、淡底文字、描边、控件边界。组件按语气换色只经这条轴,不自行挑原语。取用方式与全表见 皮肤与样式分层 · 在自定义节点上使用语气。
对比度判据
判据写在 packages/design/tokens/tests/contrast.spec.ts,改令牌先过它:
| 组合 | 门槛 |
|---|---|
| 正文 / 次要文字对画布、面、淡底 | ≥ 4.5:1(WCAG 1.4.3 AA) |
| 焦点环对画布、面、品牌淡底 | ≥ 3:1(WCAG 1.4.11) |
| 语气实心底与淡底上的文字 | ≥ 4.5:1,六族 × 明暗逐一核 |
控件边界 --xh-border-control | 缺省档与装饰边同色(1.26:1,设计决定);data-contrast="more" 下 ≥ 3:1;悬停档按棘轮不许更淡 |
| 装饰边 default / subtle / strong | 不在 1.4.11 范围内,但按棘轮钉住,不许悄悄变淡 |
| 禁用文字 | 1.4.3 豁免,只钉住对画布不低于 2.5:1 |
控件边界这一条是刻意的取舍:输入框壳、勾选框、单选圈与旁边的浮层面板、卡片描边同一重量,页面里只有一种边线;缺省档靠占位文字、标签与聚焦环辨认控件,需要 3:1 边界的场景打开高对比档。
数据色板
热力图这类按颜色点名的组件走 data-palette 轴,六个色板各取一族的满档:green(success 600)、blue(info 600)、orange(warning 600)、purple(基础色板 purple 600)、red(danger 600)、gray(中性 600,深色档换 450)。色阶从 --xh-bg-subtle 到满档逐档明度严格单调,明暗两套都验过。更多颜色点名的场景直接取基础色板。
