@gitbook/colors:GitBook 色彩体系与 Oklch 色阶生成引擎的源码级解析
@gitbook/colors:GitBook 色彩体系与 Oklch 色阶生成引擎的源码级解析
@gitbook/colors 是 GitBook 开源前端(本仓库 packages/colors)中负责颜色体系的核心包:它既定义了一套默认品牌色与语义色(成功、警告、危险、信息等),也提供了将任意一个十六进制颜色扩展为完整可配色阶(color scale)、Tailwind 色阶与对比度文本色的变换函数。读完本文,你将掌握 GitBook 前端从"一个品牌色"到"一整套 UI 色板"的完整链路,理解其基于 Oklch/Oklab 色彩空间的底层原理,并能在自己的主题定制场景中直接复用这些 API。
包定位与使用方式
从 packages/colors/README.md 可以看到,该包的定义极其简洁:@gitbook/colors 是 "A set of default colors and transformation functions used throughout the GitBook Open and app."——即一套默认颜色与变换函数,贯穿 GitBook Open 与官方 App 使用。它被 packages/gitbook 大量引用,包括 tailwind.config.ts、CustomizationRootLayout.tsx、ogimage.tsx、icon.tsx 等文件,是站点自定义配色能力的底层依赖。
包入口 packages/colors/src/index.ts 只做两件事:export * from './colors' 与 export * from './transformations',即默认色常量与变换函数两大部分。构建与发布配置见 packages/colors/package.json:ESM 模块("type": "module")、tsdown 构建、sideEffects: false(便于 tree-shaking)、发布产物包含 dist、README.md 与 CHANGELOG.md。
默认颜色:GitBook 生态的品牌与语义基调
packages/colors/src/colors.ts 定义了整个 GitBook 前端的颜色基准,共 7 个常量:
| 常量 | 值 | 语义 |
|---|---|---|
DEFAULT_PRIMARY_COLOR |
#346DDB |
GitBook 生态默认主色 |
DARK_BASE |
#1D1D1D |
色阶的相对最小值(最暗端) |
LIGHT_BASE |
#FFFFFF |
色阶的相对最大值(最亮端) |
DEFAULT_TINT_COLOR |
#787878 |
未被主色覆盖的 UI 元素基准色(中性灰,可被站点定制覆盖) |
DEFAULT_HINT_INFO_COLOR |
#787878 |
信息类消息与中性提醒 |
DEFAULT_HINT_WARNING_COLOR |
#FE9A00 |
重要信息或非关键警告 |
DEFAULT_HINT_DANGER_COLOR |
#FB2C36 |
破坏性操作或关键告警 |
DEFAULT_HINT_SUCCESS_COLOR |
#00C950 |
积极操作或成就反馈 |
值得注意的两点设计:
DARK_BASE/LIGHT_BASE是所有生成色阶的相对端点(注释原文:the relative minimum/maximum of every generated color scale),不是简单的"黑白"概念,而是色阶插值的目标锚点;DEFAULT_TINT_COLOR与DEFAULT_HINT_INFO_COLOR同为#787878,但用途不同——前者是所有非主色 UI 元素的默认基调色(可被站点定制覆盖),后者专门服务于信息提示条。
色阶生成:colorScale 的 Oklch 原理
色阶生成是 @gitbook/colors 的核心能力,实现在 packages/colors/src/transformations.ts 的 colorScale(hex, options) 函数中,生成的是 Radix 风格(1~12 步)的完整色阶,返回一个 12 元素的十六进制颜色数组。
选项参数
ColorScaleOptions(transformations.ts#L168-L193)支持:
darkMode?: boolean:默认false。为true时反转色阶(1 号步为黑而非白),并使用colorMixMapping.dark中不同的每步混合比例;background?: string:自定义背景色,缺省时使用全局LIGHT_BASE/DARK_BASE(亮/暗模式各自对应);foreground?: string:自定义前景色,缺省同上;baseStep?: number:从 1 开始的色阶步号,标记该色阶渲染为页面背景的那一步(clean主题为 1 =tint-base,muted主题为 2 =tint-subtle)。设置后,极端近中性色调会被当作精确背景并锚定到该步。不定义页面背景的色阶(如 accents,或双色bold主题)不传此项,永不锚定;mix?: { color: string; ratio: number }:将指定颜色按ratio额外混入生成的色阶。
每步混合比例(colorMixMapping)
transformations.ts#L79-L83 定义了每个色阶步骤的前景/背景亮度混合比例,注释将其概括为"0 = 100% 背景亮度(亮色模式下即白色),1 = 100% 前景亮度(亮色模式下即黑色)":
light: [0, 0.02, 0.03, 0.05, 0.07, 0.1, 0.15, 0.2, 0.5, 0.55, 0.6, 1],
dark: [0, 0.03, 0.08, 0.1, 0.13, 0.15, 0.2, 0.25, 0.5, 0.55, 0.75, 1],
从源码看,数组下标与语义类别严格对应:// bgs |components |borders |solid |text——即第 1 步(背景 base)、第 2 步(背景 subtle)、第 3~5 步(组件 DEFAULT/hover/active)、第 6~8 步(边框 subtle/DEFAULT/hover)、第 9 步(实色 solid)、第 10 步(solid-hover)、第 11~12 步(文本 DEFAULT/strong)。暗色模式的混合比例整体更高,以补偿深色背景下人眼对明度差异感知的变化。
色度(Chroma)控制策略
colorScale 对色阶不同位置采用不同的 chroma 衰减系数(transformations.ts#L274-L291):
- 第 9、10 步(实色背景):
chromaRatio = 1,完全饱和(注释:Step 9 and 10 have max chroma); - 第 11 步(文本):
0.4;第 12 步(强调文本):0.1——文本色逐步去饱和,这是 CHANGELOG 中 0.3.2 版本"通过降低色阶末段 chroma 使文本色去饱和"的落地实现; - 其他步骤默认
index * 0.05线性递增;当处于 exact base 场景时,从 base 向 accents 的步骤保持 base 的 chroma(背景保持色调),比 base 更亮的步骤(如muted主题的卡片)继续向白色去饱和。
第 9 步还有一处"就近使用原色"的优化(transformations.ts#L264-L272):当无 mix 且原色明度与目标第 9 步明度差小于 0.2 时,直接输出原色作为第 9 步——这正是 CHANGELOG 0.3.3 中 Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast(放宽第 9 步明度判断,允许输入色带更高对比度)所描述的行为。
exact base:精确锚定品牌背景色
transformations.ts#L85-L99 定义了两个关键常量:
EXACT_BASE_LIGHT_THRESHOLD = 0.95:亮色模式没有"比白更白"的对应物,因此明度 ≥ 0.95 的色调会被视为显式近白背景(例如暖色调#F5F3EF,L≈0.96)。该阈值设得较高,是为了避免把明度约 0.90 的浅色 UI 强调色误判为背景;EXACT_BASE_NEUTRAL_CHROMA = 0.05:只有近中性色调才读作背景——饱和色会在锚定步保留精确色相,而其余低位步保持近灰,因此饱和色继续走普通 accent 渐变。
当 baseStep 存在、颜色位于色阶极值端且 chroma 低于阈值时,isExactBase 为真(transformations.ts#L222-L228):该颜色会被逐字锚定到 baseStep 对应的步,同时通过反解背景明度方程(transformations.ts#L239-L251)让相邻步与其保持连续,并放大前 9 步的混合差量以弥补极值端明度变化不可感知的问题。这一机制是 CHANGELOG 0.3.0 版本 Override tint lightness if supplied color is out of bounds(越界输入色时覆盖 tint 明度)与 0.4.0 版本 Add original background color step 的功能基础。
Tailwind 色阶:shadesOfColor 与语义 scale
shadesOfColor:生成 50~900 的 Tailwind 色阶
shadesOfColor(hex, halfShades = false)(transformations.ts#L125-L166)将一个 hex 颜色扩展为 Tailwind 风格色阶:基础档位 50, 100, 200, 300, 400, 500, 600, 700, 800, 900,可选 halfShades 追加 150, 250, 350, 450, 550, 650, 750, 850 半档。500 直接返回原色;大于 500 的深档从 DARK_BASE 向原色插值,小于 500 的浅档从原色向 LIGHT_BASE 插值,插值函数 getColor(percentage, start, end)(transformations.ts#L346-L352)对 RGB 三通道做线性混合。
在 packages/gitbook/tailwind.config.ts#L33-L48 中,generateShades 直接消费该函数:把 50~900 各档转换成 rgb(r g b / <alpha-value>) 形式(借助 hexToRgb),并将 DEFAULT 映射到 500 档。也就是说,站点自定义主色会先经 shadesOfColor 变成可被 Tailwind 工具类引用的完整色阶。
语义 scale 与 ColorCategory
scale(transformations.ts#L31-L72)把 12 步色阶按语义映射为五类,注释说明每个 ColorCategory 都可以在 Tailwind 工具类生成时被包含或排除:
backgrounds:base → 1(基础背景)、subtle → 2(强调背景);components:DEFAULT → 3、hover → 4、active → 5;borders:subtle → 6(细微边框/分隔线)、DEFAULT → 7(元素边框/焦点环)、hover → 8;accents:solid → 9(实色背景)、solid-hover → 10、original → 'original'(原色,0.4.0 新增);text:subtle → 9(极低对比文本,源码注释明确警告:this contrast does not meet accessibility guidelines,需配合contrast-more样式使用)、DEFAULT → 11(低对比文本)、strong → 12(高对比文本)。
注意 text.subtle 映射到第 9 步而非 11——语义上"subtle 文本"与"实色背景"共享色阶步,这是 Radix 风格色阶的典型复用设计。generateVarShades(tailwind.config.ts#L15-L31)遍历该 scale,为每个语义键生成 rgb(var(--var-${value})) 的 CSS 变量引用,从而把语义色与设计令牌打通。
颜色空间转换与对比度工具
完整的色彩空间转换链
transformations.ts#L354-L428 提供了一套可独立使用的颜色空间转换函数,这也是整个包"从 hex 到 Oklch、再回到 hex"的主干:
rgbToLinear/linearToRgb:sRGB 与线性 RGB 互转(含 gamma 校正);rgbToOklab/oklabToRgb:线性 RGB 与 Oklab 互转;oklabToOklch/oklchToOklab:Oklab 与 Oklch 互转(C为彩度,H为色相角,负数归一到 0~360);rgbToOklch/oklchToRgb:组合上述步骤的便捷入口;rgbToXyz/xyzToLab65/rgbTolab65:基于 D65 参考白(D65 = [95.047, 100.0, 108.883])的 XYZ/Lab 转换,服务于对比度计算。
colorScale 内部正是把输入 hex 转成 Oklch(baseColor),在混合亮度与 chroma 比例后经 oklchToRgb 回写 hex(transformations.ts#L210-L213、#L299),保持色相不变而只调整明度与彩度——这正是 Oklch 空间做感知均匀渐变的核心优势。
文本对比度:colorContrast 与 dpsContrast
colorContrast(background, foreground = <a href="https://link.gitcode.com/i/0b257dbf69d3a875799e59af2fcecd79" target="_blank">LIGHT_BASE, DARK_BASE])([transformations.ts#L472-L493)在给定前景色候选中挑选与背景对比度最高的颜色,默认在纯白与 DARK_BASE(#1D1D1D)之间二选一,用于自动决定"背景上应该用深色还是浅色文字"。其对比度计算使用 dpsContrast(transformations.ts#L464-L470),即 Andrew Somers 的 Delta Phi Star perceptual lightness contrast 算法:对两色的 Lab65 明度 L 取 PHI = 0.5 + √1.25 ≈ 1.618(黄金比例)次幂后求差,再按 dps ** (1/PHI) * √2 - 40 折算,低于 7.5 的差值归零。这正是 CHANGELOG 0.4.0 中 Fix return type for colorContrast(修复返回类型,改为返回最终 hex 字符串而非 RGB 数组)对应的函数。
配套的常用输出函数还有 hexToRgb(hex)(返回空格分隔的 r g b,供 Tailwind rgb(var(--x) / <alpha-value>) 语法使用)与 hexToRgba(hex, alpha)(返回 rgba(r, g, b, alpha) 字符串);hexToRgbArray 支持 3 位与 6 位 hex,并在非法输入时抛出 Invalid hex color provided: ...(transformations.ts#L310-L331)。
测试验证:exact base 行为的边界矩阵
packages/colors/src/transformations.test.ts(使用 bun:test)针对 colorScale 的 exact base 行为构建了完整的边界用例矩阵,是理解该机制行为边界的权威依据:
- 极浅色调
#F5F3EF在baseStep: 1时被逐字作为第 1 步输出(clean/bold主题); - 同一颜色在
baseStep: 2时锚定到第 2 步,且第 1 步"恰好位于精确背景之上、向白色方向"(muted主题); - 暗色模式
#0B0F19配darkMode: true, baseStep: 1可锚定,且保留色相与彩度; - 中明度色调
#787878、饱和浅色#FFEB3B、浅灰蓝#D8DEEC(L≈0.90,UI 强调色而非背景)均不触发锚定,第 1 步保持白色; - 自定义背景
#f8f8f8时,输入色#eeeeee不再是极值端,传入的 background 被尊重而非被覆盖; - 即使传入中性
mix(#787878比例 0.4,模拟 tint 与主色相同被混色的场景),近白精确背景依然锚定,不会被混暗; - 完全不传
baseStep时永不锚定(accent 色阶与bold双色主题)。
这些测试与 CHANGELOG 中 0.3.0(越界明度覆盖)、0.3.3(第 9 步明度判断放宽)、0.4.0(新增 original 背景步)的变更一一呼应,验证了色阶引擎对"品牌色即页面背景色"场景的精确支持。
版本演进脉络(CHANGELOG 解读)
packages/colors/CHANGELOG.md 完整记录了该包的演进历史(当前版本 0.4.3):
- 0.2.0(Initial release):包首次发布;
- 0.3.0(Minor):Override tint lightness if supplied color is out of bounds——为越界输入色覆盖 tint 明度(exact base 机制的前身);
- 0.3.1(Patch):降低色阶第 1 步的 chroma,避免浅档过于鲜艳;
- 0.3.2(Patch):通过降低色阶末段 chroma 使文本色去饱和,改善大字号文本的观感;
- 0.3.3(Patch):更新每步 chroma 比例;修复 HTTP 代码高亮;放宽第 9 步明度判断,允许输入色以更高对比度直接作为第 9 步;
- 0.4.0(Minor):新增
original背景色步(即scale.accents.original);修复colorContrast返回类型; - 0.4.1(Patch):重新发布包;
- 0.4.2(Patch):标记
sideEffects: false并修复所有包的构建产物(对应 package.json 中"sideEffects": false的配置),利于打包器 tree-shaking; - 0.4.3(Patch):改用 NPM Trusted Publishing 发布流程(见 scripts/publish-if-new.sh 的调用配置)。
从这一演进可以看出,色阶引擎的打磨方向始终围绕三点:色阶两端(第 1 步与第 9 步)的色彩保真、文本步骤的感知对比度、以及发布与构建的工程化质量。
小结
@gitbook/colors 以极小的 API 表面(默认色常量 + colorScale/shadesOfColor/colorContrast/颜色空间转换工具)实现了 GitBook 前端完整的动态配色能力:任意品牌色经 Oklch 空间的明度混合与 chroma 控制,生成 Radix 风格 12 步语义色阶;shadesOfColor 输出 Tailwind 工具类色阶;colorContrast 借助 Delta Phi Star 算法自动决定前景文本颜色;exact base 机制则让"品牌色即页面背景"的场景获得逐字精确的锚定。无论是阅读 transformations.ts 理解色阶原理,还是参考 transformations.test.ts 的边界用例、tailwind.config.ts 的接入方式,都能快速在自有主题定制中复用这套经过验证的色彩工程方案。