@gitbook/colors:GitBook 色彩体系与 Oklch 色阶生成引擎的源码级解析

原创2026-09-30 18:46:101,463 阅读
文章标签:前端后端知识管理

@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 的接入方式,都能快速在自有主题定制中复用这套经过验证的色彩工程方案。

登录后查看全文
gitbook