@vertekum/ext-token-ramp
v0.3.15
Published
Vertekum extension: generate colour ramps from a single brand anchor — a fixed lightness ladder with chroma arched through the anchor
Maintainers
Readme
@vertekum/ext-token-ramp
Colour ramps generated from a single brand anchor. A group in the token collection declares a ramp; this extension computes its stops — a fixed lightness ladder with chroma arched through the anchor, in OKLCH.
Get started
// vertekum.config.ts
import { defineConfig } from '@vertekum/core';
import { tokenRampExtension } from '@vertekum/ext-token-ramp';
export default defineConfig({
collection: './tokens',
extensions: [tokenRampExtension],
});// tokens/core.json — the ramp is a group carrying a payload; the stops are computed
{
"brand": {
"accent": { "$type": "color", "$value": { "colorSpace": "oklch", "components": [0.6884, 0.1148, 188.2], "alpha": 1, "hex": "#1DB1A8" } }
},
"color": {
"teal": {
"$extensions": {
"org.vertekum.generate/ramp": {
"anchor": "{brand.accent}",
"scalar": "100-1000/100"
}
}
}
}
}color.teal.100 … color.teal.1000 now exist — aliasable, exportable, validated —
without being written anywhere. That is virtual mode. To write them as real
tokens instead (committed mode):
# vertekum ramp build [--check] [--dry-run] [--json]
vertekum ramp build # writes the stops under each ramp group
vertekum ramp build --check # CI guard: fails when committed stops are stale
vertekum ramp build --dry-run --json # emits every computed stop as data.ramps —
# a value source that needs no CSS parsingA ramp group may carry its own $root token (the anchor swatch as the group's
value): the root parses as usual and the generated stops appear beside it.
The payload stays the single source either way: virtual stops refuse direct edits
("generated by 'org.vertekum.generate/ramp' — edit the payload"), and committed
stops are regenerated by ramp build.
The payload
| Field | Values | Required | Meaning |
| --- | --- | --- | --- |
| anchor | a colour ("#1DB1A8", a stored colour object) or a reference ("{brand.accent}") | yes | The brand colour the ramp is built from. It is never repainted — its step carries it verbatim. |
| scalar | a stepped range, "min-max/step" ("100-1000/100", "050-500/50" — leading zeros pad the names) | yes | The step names. The anchor's position is derived, never declared: it lands on the step whose ladder lightness is nearest its own. |
| hueDrift | degrees (number) | no (default 0) | Dark-side hue rotation, reached in full at the last step. Deep yellows drift toward orange instead of going olive. |
| profile | a name from profiles | no | Which physics profile this ramp uses. Absent → defaultProfile → the base settings alone. An unknown name is an error, never a fallback. |
| ladder, lightness, lightFraction, darkExponent | as in settings | no | Per-ramp overrides of the configured physics. |
Settings
How a shade is calculated — the full walkthrough with a worked example is in docs/ramp.md.
| Setting | Default | Meaning |
| --- | --- | --- |
| lightness.first | 0.958 | The palest step's lightness (OKLCH L, 0–1). |
| lightness.last | 0.27 | The deepest step's lightness. |
| lightness.ease | 1 | How the steps travel between them: 1 = evenly spaced, >1 bunches the light end, <1 the dark end. |
| ladder | — | An explicit step name → L table. Wins over the curve for the steps it names — use it to reproduce a hand-tuned ladder exactly. |
| lightFraction | 0.2 | How washed-out the palest step is: its chroma is this fraction of the anchor's. |
| darkExponent | 0.85 | How quickly chroma falls on the dark side (higher = duller deep shades). |
| profiles | — | Named partials of the fields above — one per brand, typically. A payload selects one with "profile": "<name>". Resolution is per field: defaults ← top-level settings ← profile ← payload overrides, ladders merging by step key. |
| defaultProfile | — | The profile a payload without profile uses. |
// Multi-brand: declare each brand's physics once; each ramp payload says one word.
extensions: [
tokenRampExtension({
profiles: {
brand-a: { ladder: { '100': 0.958, '200': 0.91, /* … */ '1000': 0.27 } },
brand-b: { lightness: { first: 0.97, last: 0.24, ease: 1.15 } },
},
defaultProfile: 'brand-a',
}),
],What check enforces
- The payload's shape (its own schema, diagnostics under the
ramp/*domain). ramp/invalid-scalar— the range does not parse or yields fewer than two steps.ramp/unknown-profile— the payload (ordefaultProfile) names a profile the settings do not define; the message lists what is defined.ramp/unresolved-anchor— the anchor reference dangles or is not a colour. A virtual ramp must be loud when it cannot compute, never silently absent.
License
Apache-2.0
