简体中文
色彩
颜色分三层:基础色板(primitive,只声明一次的原始值)、语义角色(semantic,bg-* / fg-* / border-* / ring-*,皮肤只消费这一层)、组件覆盖槽(--xh-<组件>-*)。这一页讲前两层怎么定,以及它们之间的对比度判据。产物与引入方式见设计令牌与主题。
色彩模型
所有颜色写成 oklch(L C H),不用 hex / hsl。OKLCH 的明度 L 是感知均匀的:同一 L 的不同色相看起来一样亮,对比度只由 L 决定。于是整套色板可以只定一条明度曲线,换色相不改对比度;深色主题不是逐色手调,而是沿同一条曲线反向取档。
基础色板
十二个色相,每个色相 11 档(50 – 950)。每档的明度与彩度取品牌曲线的基线,彩度再按该档明度与色相收进 sRGB 色域。色相 258 那一族(indigo)就是这条基线本身,也是原来的缺省品牌,现在作为 data-brand="indigo" 预设保留;缺省品牌换成了色相 277 的靛蓝(见品牌色)。
50oklch(0.971 0.014 25)
100oklch(0.936 0.032 25)
200oklch(0.885 0.062 25)
300oklch(0.809 0.105 25)
400oklch(0.702 0.165 25)
500oklch(0.623 0.214 25)
600oklch(0.546 0.216 25)
700oklch(0.488 0.196 25)
800oklch(0.424 0.164 25)
900oklch(0.379 0.132 25)
950oklch(0.282 0.089 25)
50oklch(0.971 0.014 50)
100oklch(0.936 0.032 50)
200oklch(0.885 0.062 50)
300oklch(0.809 0.105 50)
400oklch(0.702 0.165 50)
500oklch(0.623 0.214 50)
600oklch(0.546 0.216 50)
700oklch(0.488 0.196 50)
800oklch(0.424 0.164 50)
900oklch(0.379 0.132 50)
950oklch(0.282 0.089 50)
50oklch(0.971 0.014 70)
100oklch(0.936 0.032 70)
200oklch(0.885 0.062 70)
300oklch(0.809 0.105 70)
400oklch(0.702 0.165 70)
500oklch(0.623 0.18 70)
600oklch(0.546 0.183 70)
700oklch(0.488 0.196 70)
800oklch(0.424 0.164 70)
900oklch(0.379 0.132 70)
950oklch(0.282 0.089 70)
50oklch(0.971 0.014 95)
100oklch(0.936 0.032 95)
200oklch(0.885 0.062 95)
300oklch(0.809 0.105 95)
400oklch(0.702 0.165 95)
500oklch(0.623 0.17 95)
600oklch(0.546 0.171 95)
700oklch(0.488 0.184 95)
800oklch(0.424 0.164 95)
900oklch(0.379 0.132 95)
950oklch(0.282 0.089 95)
50oklch(0.971 0.014 125)
100oklch(0.936 0.032 125)
200oklch(0.885 0.062 125)
300oklch(0.809 0.105 125)
400oklch(0.702 0.165 125)
500oklch(0.623 0.206 125)
600oklch(0.546 0.206 125)
700oklch(0.488 0.196 125)
800oklch(0.424 0.164 125)
900oklch(0.379 0.132 125)
950oklch(0.282 0.089 125)
50oklch(0.971 0.014 149)
100oklch(0.936 0.032 149)
200oklch(0.885 0.062 149)
300oklch(0.809 0.105 149)
400oklch(0.702 0.165 149)
500oklch(0.623 0.214 149)
600oklch(0.546 0.205 149)
700oklch(0.488 0.196 149)
800oklch(0.424 0.164 149)
900oklch(0.379 0.132 149)
950oklch(0.282 0.089 149)
50oklch(0.971 0.014 180)
100oklch(0.936 0.032 180)
200oklch(0.885 0.062 180)
300oklch(0.809 0.105 180)
400oklch(0.702 0.147 180)
500oklch(0.623 0.138 180)
600oklch(0.546 0.131 180)
700oklch(0.488 0.13 180)
800oklch(0.424 0.132 180)
900oklch(0.379 0.132 180)
950oklch(0.282 0.089 180)
50oklch(0.971 0.014 215)
100oklch(0.936 0.032 215)
200oklch(0.885 0.062 215)
300oklch(0.809 0.105 215)
400oklch(0.702 0.143 215)
500oklch(0.623 0.135 215)
600oklch(0.546 0.129 215)
700oklch(0.488 0.128 215)
800oklch(0.424 0.132 215)
900oklch(0.379 0.132 215)
950oklch(0.282 0.089 215)
50oklch(0.971 0.014 237)
100oklch(0.936 0.032 237)
200oklch(0.885 0.062 237)
300oklch(0.809 0.105 237)
400oklch(0.702 0.165 237)
500oklch(0.623 0.169 237)
600oklch(0.546 0.163 237)
700oklch(0.488 0.163 237)
800oklch(0.424 0.164 237)
900oklch(0.379 0.132 237)
950oklch(0.282 0.089 237)
50oklch(0.971 0.014 258)
100oklch(0.936 0.032 258)
200oklch(0.885 0.062 258)
300oklch(0.809 0.105 258)
400oklch(0.702 0.165 258)
500oklch(0.623 0.214 258)
600oklch(0.546 0.216 258)
700oklch(0.488 0.196 258)
800oklch(0.424 0.164 258)
900oklch(0.379 0.132 258)
950oklch(0.282 0.089 258)
50oklch(0.971 0.014 302)
100oklch(0.936 0.032 302)
200oklch(0.885 0.062 302)
300oklch(0.809 0.105 302)
400oklch(0.702 0.165 302)
500oklch(0.623 0.214 302)
600oklch(0.546 0.216 302)
700oklch(0.488 0.196 302)
800oklch(0.424 0.164 302)
900oklch(0.379 0.132 302)
950oklch(0.282 0.089 302)
50oklch(0.971 0.014 345)
100oklch(0.936 0.032 345)
200oklch(0.885 0.062 345)
300oklch(0.809 0.105 345)
400oklch(0.702 0.165 345)
500oklch(0.623 0.214 345)
600oklch(0.546 0.216 345)
700oklch(0.488 0.196 345)
800oklch(0.424 0.164 345)
900oklch(0.379 0.132 345)
950oklch(0.282 0.089 345)
怎么读这张色板:
- 同一档跨色相同一明度。 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 调亮。
- 皮肤不直接消费色板。 组件皮肤只认语义角色、语气轴与图表数据色(
--xh-chart-*);色板给使用者、按颜色点名的色板轴与自定义语气用,图表数据色也从它算出(见数据色板)。
色板由 packages/design/tokens/build/emit-palette.mjs 从 tokens/palette.seeds.json 的十二个色相角派生,改色相只改种子;tests/palette.spec.ts 逐档核对生成物、明度与运行时 deriveBrandScale 同源。
中性色板
十六档,含 450 / 550 / 650 / 750 四个半档:中性色承担文字、背景、边界三种角色,正文与次要文字、装饰边与控件边、面与抬起的面之间常常只差半档,整档跳会太跳。
0oklch(1 0 0)
50oklch(0.979 0.003 265)
100oklch(0.964 0.004 265)
200oklch(0.922 0.007 270)
300oklch(0.87 0.011 262)
400oklch(0.7 0.016 258)
450oklch(0.649 0.018 253)
500oklch(0.556 0.018 255)
550oklch(0.52 0.018 256)
600oklch(0.461 0.018 258)
650oklch(0.4 0.006 258)
700oklch(0.371 0.006 258)
750oklch(0.28 0.006 258)
800oklch(0.269 0.006 258)
900oklch(0.208 0.006 258)
950oklch(0.145 0.005 258)
| 档 | 浅色主题里的角色 | 深色主题里的角色 |
|---|---|---|
| 0 | 面(--xh-bg-surface) | — |
| 50 | — | 正文 |
| 100 | 页面底(--xh-bg-page):白面放在冷灰底上 | — |
| 400 / 550 / 600 | 禁用字 / 次要字 / 正文次级 | 次要字 / — / 禁用字 |
| 750 | — | 抬起的面(--xh-bg-surface-raised) |
| 900 / 950 | 正文(900) | 面(900)与页面底(950) |
淡底与三档边线不取这张表里的灰,取墨色按比例透明(浅色档淡底 4.7% / 10.2% / 16.9%、边线 subtle 4.7% / default 10.2% / strong 16.9%),见下文彩色面与墨色域。
品牌色
品牌色不是一枚颜色,是一条从种子派生出来的 11 档梯度。派生只取种子的色相与彩度,明度曲线原样保留——语义层与语气层建立在明度之上的对比度保证,对任何种子都成立。
缺省品牌是 OKLCH 色相 277 的靛蓝。它和预设品牌同一个做法:每一档的明度按基线(色相 258 那一族)那一档的 WCAG 相对亮度反求,600 档实心底上的白字是 5.08:1。没有直接取 Tailwind 的 indigo-500,是因为它上面的白字只有 4.47:1,过不了正文的 4.5:1。
50oklch(0.972 0.014 277)
100oklch(0.937 0.032 277)
200oklch(0.888 0.062 277)
300oklch(0.813 0.103 277)
400oklch(0.709 0.162 277)
500oklch(0.632 0.209 277)
600oklch(0.557 0.216 277)
700oklch(0.498 0.196 277)
800oklch(0.432 0.164 277)
900oklch(0.385 0.132 277)
950oklch(0.286 0.089 277)
ts
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 | 中性提示、进行中 |
400oklch(0.7 0.185 25)
500oklch(0.637 0.208 25)
600oklch(0.577 0.213 25)
700oklch(0.5 0.2 25)
500oklch(0.723 0.171 149)
600oklch(0.648 0.165 149)
700oklch(0.51 0.15 149)
400oklch(0.86 0.14 86)
500oklch(0.795 0.161 86)
600oklch(0.705 0.16 70)
700oklch(0.62 0.15 70)
800oklch(0.55 0.14 70)
500oklch(0.685 0.156 237)
600oklch(0.62 0.16 237)
700oklch(0.53 0.15 237)
danger 动作与 error 状态分开定义,不共用业务语义;状态色表达任务结果,不表达空间层级。
语义角色
皮肤只消费这一层。每个实色底都配了前景(--xh-fg-on-brand、--xh-tone-on),组件不自行计算文字色。取值随 data-theme 翻转,下面的色块画的是当前主题。
背景
| 令牌 | 取值 | 预览 | 说明 |
|---|---|---|---|
--xh-bg-page | var(--xh-color-neutral-100) | 页面底:面之下那一层,铺满视口 | |
--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 | color-mix(in oklab, var(--xh-ink) 4.7%, transparent) | 淡底;白底上的 hover | |
--xh-bg-subtle-hover | color-mix(in oklab, var(--xh-ink) 10.2%, transparent) | 白底上的 pressed;淡底上的 hover | |
--xh-bg-subtle-active | color-mix(in oklab, var(--xh-ink) 16.9%, transparent) | 淡底上的 pressed,只留给按下 | |
--xh-bg-field | var(--xh-bg-surface) | 字段外壳静息的底:缺省与承载面同色(白底),边界由字段描边承担;主题要淡底字段只改这一支 | |
--xh-bg-muted | color-mix(in oklab, var(--xh-ink) 4.7%, transparent) | 禁用实心钮退到的中性面 | |
--xh-bg-subtle-opaque | color-mix(in srgb, var(--xh-ink) 4.7%, var(--xh-bg-surface)) | 淡底的不透明档:要盖住下层内容的面(吸顶表头、浮动钮、层叠头像) | |
--xh-bg-subtle-hover-opaque | color-mix(in srgb, var(--xh-ink) 10.2%, var(--xh-bg-surface)) | 淡底 hover 档的不透明档 | |
--xh-bg-subtle-active-opaque | color-mix(in srgb, var(--xh-ink) 16.9%, var(--xh-bg-surface)) | 淡底 active 档的不透明档 | |
--xh-bg-muted-opaque | color-mix(in srgb, var(--xh-ink) 4.7%, var(--xh-bg-surface)) | 中性面的不透明档 | |
--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 | color-mix(in oklab, var(--xh-color-neutral-900) 40%, transparent) | 模态遮罩 |
前景
| 令牌 | 取值 | 预览 | 说明 |
|---|---|---|---|
--xh-fg-default | var(--xh-color-neutral-900) | 正文 | |
--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-800) | ||
--xh-fg-info | var(--xh-color-info-700) | ||
--xh-fg-danger | var(--xh-color-danger-700) |
边界与焦点环
| 令牌 | 取值 | 预览 | 说明 |
|---|---|---|---|
--xh-border-default | color-mix(in oklab, var(--xh-ink) 10.2%, transparent) | 一切根面外边与 raised 面描边 | |
--xh-border-default-opaque | color-mix(in srgb, var(--xh-ink) 10.2%, var(--xh-bg-surface)) | 装饰边的不透明档:压在任意内容上、必须自带浅框的部件(取色滑杆的拇指) | |
--xh-border-subtle | color-mix(in oklab, var(--xh-ink) 4.7%, transparent) | 只作内部分隔线 | |
--xh-border-strong | color-mix(in oklab, var(--xh-ink) 16.9%, transparent) | 只作高对比档与登记过的强调边:字段悬停边、勾选方框与单选圈的静息边 | |
--xh-border-control | var(--xh-border-default) | 控件边界,缺省档与 border-default 同色;高对比档才加深到 3:1 | |
--xh-border-control-hover | color-mix(in oklab, var(--xh-ink) 37.9%, transparent) | 控件悬停边 | |
--xh-border-control-focus | var(--xh-ring-focus) | 聚焦边,与焦点环同色、不随语气;字段聚焦时描边换成它,内侧再补一道 inset 线,合起来就是盒内的焦点环 | |
--xh-border-invalid | var(--xh-color-danger-600) | ||
--xh-ring-focus | var(--xh-color-brand-600) | 公共键盘焦点环,2px 画在元素自己的盒内,外侧贴着承载面,对页面底、画布、面与淡底都 ≥ 3:1 | |
--xh-ring-gap | var(--xh-bg-surface) | 实心面上环内侧的 1px 底色间隔线:取承载面底色,环两侧都是底色,压在任何颜色的面上都读得出来 | |
--xh-ring-invalid | var(--xh-color-danger-500) |
语气轴
data-tone 是六族语气的切换轴:brand、neutral、danger、warning、success、info。写在任何节点上,节点内即可取到整族 --xh-tone-*:实心底、实心底上的前景、淡底、淡底文字、描边、控件边界。组件按语气换色只经这条轴,不自行挑原语。按钮一类的动作控件上,neutral 与不写等价:描边、淡底、幽灵形态就是缺省的中性面,不另铺 neutral 淡底、不换深色描边;实心形态仍是中性那一族的实心底。Tag、Alert、Badge 这类语气面的 neutral 照常取 neutral 淡底。取用方式与全表见 皮肤与样式分层 · 在自定义节点上使用语气。
彩色面与墨色域
组件放在彩色区块上时,在区块上声明它的底色极性。域内的中性描边、淡底与交互阶梯改取墨色(浅底为纯黑、深底为纯白)按比例透明,正文、焦点环与主要动作取墨色本身。不透明的中性灰在彩色底上显著度随底色变化十几倍(neutral 200 描边在黄底上 1.06:1、黑底上 16.68:1);墨色在任何底色上显著度一致,颜色取底色自身的深浅变体。
| 声明 | 含义 |
|---|---|
data-xh-ink="dark" | 浅色底,黑墨;区块同时是浅色主题边界,语气色、表面与原生控件取浅色档 |
data-xh-ink="light" | 深色底,白墨;区块同时是深色主题边界 |
data-xh-ink="auto" + --xh-ink-surface | 由底色的相对亮度按 0.179 选墨(与语气实心底的黑白字同一分界);只决定墨色与中性装饰,语气色与表面沿用外层主题。需要相对颜色语法(Chrome 119、Firefox 128、Safari 16.4 起),更早的引擎里等于未声明 |
data-xh-ink-margin="ample" | 底色离分界足够远(黑墨时相对亮度 ≥ 0.5,白墨时 ≤ 0.05),次要文字与占位取墨色 72%;缺省时它们等于墨色,层级只靠字号与字重 |
- 没声明域的地方同样用墨色表达描边与淡底:墨色取主题极性(浅色档纯黑、深色档纯白),所以作者自己的彩色区块即使不声明,描边、分隔与淡底也是底色自身的深浅变体,只有文字需要声明域。置灰字属于文字,同样只在域里换成墨色。
- 比例不手填,按「与原中性色对比度相等」求:描边在页面底、画布、缺省面与对话框面上各求一个比例取最大值,哪种面上都不比原来淡;淡底只按缺省面求,压在上面的字与焦点环对比度不降。浅色档描边 10.2%、淡底 4.7%,与原中性色一致;深色档描边 22%(卡片面上比原来略重)、淡底 6.2%。
- 淡底是半透明的,叠在别的淡底上会加深。要盖住下层内容的面(粘性表头、固定列、浮在内容上的钮)取
-opaque档:同一比例的墨色叠在缺省面上的实色。 - 放文字的彩色面避开相对亮度 0.15–0.24:neutral 950 与 neutral 50 在这一段都到不了 4.5:1,只有纯黑、纯白勉强达标。品牌色阶 500 落在其中,浅色档的品牌实心因此取 600。
- 域内的品牌实心换成墨色实心,文字取
--xh-ink-surface(未提供时取与墨色相反的纯白 / 纯黑);选中面换成墨色 12%。语气色保留自己的实心或淡底面。 - 高对比档(
data-contrast="more")下域内描边回到实色;强制色下取系统色。 - 浮层经 Portal 渲染到文档末尾,不在域内:弹出的菜单、选择面板保持自己的材质。
- 声明了域的区块,底色取原语(如
--xh-color-indigo-800)或在区块外取值。--xh-bg-brand、--xh-fg-default这类令牌在域内被改写为墨色,区块用它们画自己的底,底色会随域翻转。
库自己渲染的彩色面自动成为域,不需要声明:实心按钮与实心标签、写了 tone 的 Tooltip 里的内容按那块面的底色取墨色(与作者的 auto 同一套规则,面自身的底色不变),ImageViewer 的看片层整层是白墨域。把快捷键、分隔线或小徽标放进这些面时,它们的描边与淡底不会是一块不透明的灰。
对比度判据
判据写在 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 边界的场景打开高对比档。
数据色板
图表颜色只经 --xh-chart-* 这一层取,按职责分六类,每类只有一种结构:分类(哪个系列)、有序(阶段与档位)、顺序(大小)、发散(高于或低于基线)、语气(好坏,复用语气轴并配图标或文字)、涨跌。图表里的文字不用系列色:数值、标签与图例文字取文字令牌,身份由旁边的色标承担。
分类
1
2
3
4
5
6
7
8
other
- 8 个色槽按系列在
series里的声明顺序分配,隐藏、筛选、排序都不重新分配。不循环、没有第 9 色:超过 8 个系列时合并成「其他」(--xh-chart-categorical-other),或拆成多张小图。 - 单系列用色槽 1;突出一个系列时,其余系列改用
--xh-chart-deemphasis。 - 写在色块内部的数值用
--xh-chart-on-categorical-N,按对比度在纯白与最深的中性档之间取一个,对色块 ≥ 4.5:1;上面色块里的序号就是用它写的。
色板不是手挑的。packages/design/tokens/build/emit-chart-palette.mjs 在基础色板上为 8 个色槽搜索色相、顺序与亮暗两套档位,同一份基础色板永远得到同一套结果;check-chart-palette 门禁再从 tokens.css 复验。缺省色板是浅色调:每个色槽都是基础色板的一档往纯白里在 oklab 里按 90% / 75% / 60% / 45% 混合,柱、扇区、面积这类大块填充读起来轻。基础色板同一档跨色相同一明度,所以色板必须跨档、跨比例取值,相邻色槽之间才有明度差——色觉障碍读者主要靠明度差分辨颜色。亮暗两套各自校验,承载面取各自的 --xh-bg-surface:
| 检查 | 门槛 |
|---|---|
| 明度带 | OKLCH L:亮色 0.72–0.84,暗色 0.68–0.80 |
| 彩度带 | 0.07 ≤ C ≤ 0.14:低于下限读成灰,高于上限在亮端发荧光 |
| 对比度 | 每个色槽对承载面 ≥ 1.6:1。浅色调到不了 3:1,系列身份由图例、色块内文字与数据表补偿 |
| 相邻色槽 | 正常视觉 ΔE ≥ 15;红色弱、绿色弱模拟下 ΔE ≥ 8 |
| 前 3 个色槽两两 | 同相邻:散点、气泡这类任意两个标记都可能挨着的形态,只有前 3 个色槽保证两两可分 |
| 任意两色 | 正常视觉 ΔE ≥ 8:隐藏一个系列后,原本不相邻的两色就挨在了一起;浅色调挤在亮端,门槛与莫兰迪柔彩同档 |
| 色相分散 | 任意 45° 扇区(8 个色槽的平均间隔)里至多 2 个色槽,同一色系不挤在一起 |
| 离开告警色 | 与 danger 语气每一档的正常视觉 ΔE ≥ 10:图里的红色会被读成告警 |
| 固定顺序 | 亮暗同一色槽同一色相,色槽 1 是品牌色相 |
满足全部检查的排法里,取相邻色槽在两种色觉障碍模拟下最小 ΔE 最大的一种。red 与发红的 orange 被「离开告警色」挡在外面;yellow 整族不取,浅色调的明度带里它在亮色发土黄、在暗色只剩橄榄色。颜色按浏览器在 sRGB 显示器上的画法换算:基础色板的中档允许略出 sRGB 色域,出界的通道逐个截断。色觉障碍的模拟用 Machado–Oliveira–Fernandes 2009(严重度 1.0),与 @xihan-ui/viz 校验色板的函数同一口径,使用者可以用它复验自己覆盖后的色板:浅色调传 tone: 'light',缺省按标准明度带(亮色 0.43–0.77、暗色 0.48–0.67,彩度 ≥ 0.10)判。
配色方案
上面的分类色板是缺省的「多彩分类」。另有三套配色方案,四套都是浅色调,在任意祖先上写 data-xh-chart-palette 即把分类色槽与色块内文字整套换掉:写在 <html> 上全站生效,写在一个容器或一张图上只换这一块。方案落在令牌层,八个图表组件、图例、提示框与画布渲染器都不用改,切换时画布跟着重画。
html
<html data-xh-chart-palette="monochrome">
…
<!-- 这一块改回缺省的多彩分类 -->
<section data-xh-chart-palette="categorical">…</section>
</html>| 取值 | 方案 | 取色 | 适合 |
|---|---|---|---|
categorical(缺省) | 多彩分类 | 上一节的 8 个色槽 | 系列多、要一眼分清 |
monochrome | 主题单色 | 品牌色阶由深到浅:亮色从 500 起逐档变浅,末两档再往承载面里混;暗色从 300 起往深走 | 仪表盘与指标卡统一成主题色;随 data-brand 换色 |
brand | 柔和品牌 | 品牌色相两侧的冷色一族加一抹粉,与多彩分类同样往纯白里混出浅色调,同一色相至多取两档 | 品牌感强、又要比单色好分辨 |
muted | 莫兰迪柔彩 | 基础色往浅灰(中性 300)里在 oklab 里按 35% / 50% / 65% 混合,带灰的浅色,彩度压到 0.035–0.10 | 信息密集或偏克制的页面 |
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
三套方案同样由生成器算出、由门禁从 tokens.css 复验,各有自己的规则:
| 方案 | 与多彩分类不同的检查 |
|---|---|
| 主题单色 | 只靠明度区分:色相随品牌、对承载面的对比度逐档递减、色槽 1 ≥ 1.6:1、前 4 个色槽相邻 ΔE ≥ 5。浅档更弱,由图例、提示框与数据表补偿;系列超过 4 个时换多彩分类 |
| 柔和品牌 | 明度带、彩度带与对比度同多彩分类;不查色相分散;冷色一族挤在亮端,相邻色槽放宽到正常视觉 ΔE ≥ 12、色觉障碍 ≥ 6;同一色相的两档只差一档明度,任意两色放宽到 ΔE ≥ 7 |
| 莫兰迪柔彩 | 彩度 0.035–0.10;明度带亮色 0.70–0.82、暗色 0.64–0.76;对比度同多彩分类;相邻正常视觉 ΔE ≥ 12、色觉障碍 ≥ 6;任意两色 ≥ 8;任意 45° 扇区至多 3 个色槽 |
离开告警色与色块内文字 ≥ 4.5:1 三套都照常检查。有序与顺序色阶本来就是品牌色相,data-brand 换色时一起换,不随方案切换。
有序、顺序与发散
1
2
3
4
5
6
start
mid
end
negative
center
positive
- 有序:浅色调,亮色从品牌 500 起、暗色从 300 起,相邻两档之间补半档(两档在 oklab 里各半混合);单色相,1 对承载面最强、逐档减弱,最弱一档仍 ≥ 1.5:1。
- 顺序:浅色调,单色相由浅到深,亮色锚点取品牌 50 / 300 / 500,小值贴近承载面,终点仍 ≥ 3:1;暗色下锚点翻转。压在色块上的文字(层级图按值着色时的名字)按段取
--xh-chart-on-sequential-low/-high:亮色两段都写正文色,暗色高段贴近亮端、写承载面色。标记只带自己在色阶上的位置,皮肤用color-mix(in oklch, …)在锚点之间插值,切换主题时颜色跟着令牌走,不由脚本算色。 - 发散:amber 与 blue 两臂同档(亮色 500、暗色 400),同一幅度读起来一样重;两端仍 ≥ 3:1;中点是贴近承载面的中性色。插值用
in oklab:中点没有色相,oklch 插值会绕着色相环走出一段杂色。
涨跌与图表家具
| 令牌 | 取值 | 预览 | 说明 |
|---|---|---|---|
--xh-chart-rise | color-mix(in oklab, var(--xh-color-success-600) 50%, var(--xh-color-neutral-0)) | 上涨:缺省成功色相往纯白里混出的浅色调 | |
--xh-chart-fall | color-mix(in oklab, var(--xh-color-danger-600) 70%, var(--xh-color-neutral-0)) | 下跌:缺省危险色相的浅色调,与上涨在色觉障碍模拟下靠明度拉开 | |
--xh-chart-deemphasis | var(--xh-color-neutral-300) | 强调一个系列时其余系列的颜色,比任何色槽都弱 | |
--xh-chart-surface | var(--xh-bg-surface) | 承载面:相邻填充之间的间隙与点外的描边环取它,放进淡底容器时由宿主改写 | |
--xh-chart-grid | var(--xh-border-subtle) | 网格线,内部分隔;高对比档升到装饰边 | |
--xh-chart-axis | var(--xh-border-default) | 轴线与刻度线;高对比档升到强调边 | |
--xh-chart-label | var(--xh-fg-muted) | 轴标签与数据标签 | |
--xh-chart-crosshair | var(--xh-fg-subtle) | 折线图的十字准线 | |
--xh-chart-band-highlight | var(--xh-bg-subtle) | 柱图按类目悬停时整条类目带的淡底 |
涨跌缺省绿涨红跌,两色都是往纯白里混出的浅色调,对承载面 ≥ 1.6:1、在色觉障碍模拟下靠明度拉开。要换成红涨绿跌,在主题边界上对调两支令牌,混合比例保持不变,两色的对比度与可分性就仍然成立:
css
:root,
[data-theme="light"] {
--xh-chart-rise: color-mix(in oklab, var(--xh-color-danger-600) 70%, var(--xh-color-neutral-0));
--xh-chart-fall: color-mix(in oklab, var(--xh-color-success-600) 50%, var(--xh-color-neutral-0));
}
[data-theme="dark"] {
--xh-chart-rise: color-mix(in oklab, var(--xh-color-danger-600) 80%, var(--xh-color-neutral-0));
--xh-chart-fall: color-mix(in oklab, var(--xh-color-success-600) 60%, var(--xh-color-neutral-0));
}按颜色点名的色板轴
热力图这类按颜色点名的组件走 data-palette 轴,六个色板各取一族的满档:green(success 600)、blue(info 600)、orange(warning 600)、purple(基础色板 purple 600)、red(danger 600)、gray(中性 600,深色档换 450)。色阶从 --xh-bg-subtle 到满档逐档明度严格单调,明暗两套都验过。更多颜色点名的场景直接取基础色板。
