@cssints/ramp

中文文档
0.35.1发布于 2026/11/07MITTypeScriptES Module构建来源已验证近 30 天下载 7158

OKLCH colour ramps for cssints as DTCG data: a lightness ramp, or steps picked by WCAG 2 contrast against a background

原始使用文档
文档目录4 节
  1. Modes
  2. Contrast
  3. Limits
  4. Credits

@cssints/ramp

OKLCH colour ramps for cssints, computed at build time. ramp(base, options) returns plain DTCG data (a colour group, steps "1" to "N", each a hex colour in sRGB) that you pass to createTokens yourself, and overrides(ramp) turns a ramp into the override object of createGlobalTheme. The package has no runtime and no scope(): like the sets of @cssints/tokens, a ramp is data, so it goes under any group name, sits beside other tokens in one call, and its steps get @property rules, themes and check.contrast from cssints.

// tokens.ts
import { createGlobalTheme, createTokens, media } from "cssints" with { type: "cssints" };
import { overrides, ramp } from "@cssints/ramp";

const ratios = [1.2, 1.5, 3, 4.5, 7] as const;

export const t = createTokens({
  bg: { $type: "color", $value: "#ffffff" },
  blue: ramp("#2563eb", { mode: "contrast", ratios, background: "#ffffff" }),
  gray: ramp("#6b7280", { mode: "lightness", steps: 9 }),
});

// the same ratios on the dark background, under the same condition as it
createGlobalTheme(t, media("(prefers-color-scheme: dark)"), {
  bg: "#111111",
  blue: overrides(ramp("#2563eb", { mode: "contrast", ratios, background: "#111111" })),
});
// text.ts
import { bg, check, cn, color } from "cssints" with { type: "cssints" };

import { t } from "./tokens.ts";

check.contrast(t.blue._3, t.bg, { min: 3 }); // borders, large text
check.contrast(t.blue._4, t.bg, { min: 4.5 }); // body text, light and dark
export const text = cn(color(t.blue._4), bg(t.bg));

The sheet of text.ts:

@property --bg {
  syntax: "<color>";
  inherits: true;
  initial-value: #ffffff;
}
@property --blue-4 {
  syntax: "<color>";
  inherits: true;
  initial-value: #356fec;
}
@layer _.k {
  @media (prefers-color-scheme: dark) {
    :root {
      --bg: #111111;
      --blue-1: #041e59;
      --blue-2: #092d7c;
      --blue-3: #1855db;
      --blue-4: #3d76ed;
      --blue-5: #719df2;
    }
  }
}
@layer _.a {
  ._1od4w7v {
    color: var(--blue-4);
  }
  ._q77new {
    background-color: var(--bg);
  }
}

Modes

Every step keeps the base's OKLCH hue and its chroma relative to the sRGB gamut (nutelch's idea): the base's chroma over the most chroma sRGB holds at the base's lightness and hue. A step at another lightness takes the same share of the most sRGB holds there, so a vivid base stays vivid near its hue's cusp, thins out towards white and black, and every step is in sRGB by construction (the hex is the 8-bit rounding of an in-gamut colour, never a clipped one). A grey base gives a neutral ramp. The base itself need not be a step.

  • { mode: "lightness", steps, lightness? }: steps colours spaced evenly in OKLCH lightness, from lightness[0] to lightness[1] (default [0.97, 0.25], lightest first). #2563eb with 9 steps: #f1f5fe, #c6d8f9, #9cbbf5, #719df2, #467cee, #1a59e6, #1143b2, #092f81, #041b53.

  • { mode: "contrast", ratios, background } (Leonardo's idea): one step per ratio, in the order given. A step's lightness is searched (bisection, on the hex it outputs) for the colour nearest the background whose WCAG 2 ratio against it is at or above the target. The steps go darker on a light background and lighter on a dark one (the side with more room). #2563eb with [1.2, 1.5, 3, 4.5, 7]:

    Background12345
    #ffffff#e1ebfc 1.20#bfd3f9 1.51#6593f0 3.00#356fec 4.52#164ecc 7.00
    #111111#041e59 1.20#092d7c 1.50#1855db 3.02#3d76ed 4.50#719df2 7.02

The numbers are deterministic: the same base and options give the same hex on every machine (no randomness, a fixed number of bisection steps).

Contrast

What a contrast ramp promises, exactly:

  • Each step i has ratio(step, background) >= ratios[i], with the ratio computed as check.contrast computes it: culori's wcagLuminance of each colour clipped to sRGB, (L1 + 0.05) / (L2 + 0.05), not rounded. It is measured on the hex the step outputs, so check.contrast(t.blue._4, t.bg, { min: 4.5 }) passes when t.bg holds the same background. The search only keeps colours that pass, so this does not rest on the search converging.
  • By little. The step is the passing colour nearest the background along the ramp, within an 8-bit step: in the tests every step of 8 bases on 4 backgrounds is below ratio * 1.02. So a check held to more than the step was made for fails: check.contrast(t.blue._4, t.bg, { min: 4.6 }) is contrast 4.52 is below 4.6: blue.4 #356fec on bg #ffffff, in the default values, and the same in the dark theme (4.50). Check each step at its own ratio.
  • Against that background only. A ratio holds for the background the ramp was made for. On another background (a theme), make a ramp for it with the same ratios and set both under the same condition or selector, as above; check.contrast then checks each step against the theme's background.
  • Lightness ramps promise no ratio; check the pairs you use.
  • WCAG 2 only, not APCA. A WCAG 2 pass is not an APCA pass, especially for light text on dark.

Limits

  • Errors are thrown when the module is evaluated, so they are build errors: a base or background that is not a colour (@cssints/ramp: the base "banana" is not a colour) or is translucent, steps that is not a whole number from 1, a lightness outside 0..1, and a ratio out of 1..21 or out of reach on the background (ratio 21 is out of reach on #111: at most 18.88 (white)). A mid-grey background leaves little room on either side.

  • Monotone for sorted ratios. Ascending ratios give steps moving away from the background, but the ramp does not sort them for you. The lightness is OKLCH lightness, not a luminance: two hues at one lightness have different ratios.

  • sRGB only. A P3 variant (color(display-p3 …)) is not shipped: check.contrast clips a colour to sRGB, so a P3 step would be checked as another colour than the one shown, and a color() value walks the colour grammar in the types (thousands of instantiations a token) where a hex is cheap. A base outside sRGB is read with its share of chroma capped at the gamut.

  • The editor shows #${string}. A step's value is computed when the module runs, so its type is `#${string}`, not the literal hex (hover shows Token<"color", true, `#${string}`>). Its type is still a colour token, the steps are known (t.blue._6 of a five-ratio ramp is a type error), and the build checks the values.

  • Type cost (tsgo, one file per row):

    CallInstantiationsCheck time
    color("red") (the baseline)0.16 M0.09 s
    10 ramps of 10 steps (100 tokens)0.15 M0.09 s
    100 ramps of 10 steps (1,000 tokens)0.16 M0.10 s
    the same 1,000 tokens and a theme of all of them0.24 M0.13 s

    A step costs about 10 instantiations as a token (every value has the one type `#${string}`), about 80 as a theme override, so a ramp is as cheap as the hex sets of @cssints/tokens.

cd packages/ramp && bun run test        # the ramp maths, then a fixture through the engine (check.contrast passes, a tight pair fails)
cd packages/ramp && bun run typecheck   # the type test (test/types/ramp.ts)

Credits

Two ideas, written here: the chroma relative to the sRGB gamut from nutelch (MIT, David Aerne), and the search for a lightness that meets a contrast ratio from Leonardo (Apache-2.0, Adobe). The colour maths is culori (MIT, Dan Burzo), a dependency. No code is copied.

版本与发布记录

@cssints/ramp 共发布 42 个版本,首次发布于 2026/10/11;过去 12 个月发布了 42 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.35.1latest2026/11/073 天前
0.35.02026/11/073 天前
0.34.02026/11/073 天前
0.33.02026/11/046 天前
0.32.02026/11/046 天前
0.31.02026/11/047 天前

关于 @cssints/ramp 的常见问题

@cssints/ramp 是什么?

OKLCH colour ramps for cssints as DTCG data: a lightness ramp, or steps picked by WCAG 2 contrast against a background

如何安装 @cssints/ramp?

在项目目录运行 npm install @cssints/ramp,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:cssints(^0.35.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cssints/ramp 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cssints/ramp 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cssints/ramp 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cssints/ramp 可以商用吗?

@cssints/ramp 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cssints/ramp 的下载量有多少?

根据 npm 官方下载统计,@cssints/ramp 在 2026/10/11 – 2026/11/09 这 30 天内被下载 7,158 次,日均约 239 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cssints/ramp 还在维护吗?

最新版本 0.35.1 发布于 2026/11/07(3 天前);至今共发布 42 个版本,其中过去 12 个月发布了 42 个;首次发布于 2026/10/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。