Color tokens
glaze.color() creates a single adaptive color without a theme. A token resolves the same four variants as a theme color (light, dark, lightContrast, darkContrast) and has the same output methods. For a guided introduction, see Getting started; for contrast between tokens, see Contrast solving.
import { glaze } from '@tenphi/glaze';
import type {
GlazeColorToken,
GlazeColorValue,
GlazeColorInput,
GlazeFromInput,
} from '@tenphi/glaze';glaze.color(
input: GlazeColorValue | GlazeFromInput | GlazeColorInput,
config?: GlazeConfigOverride,
): GlazeColorToken;The first argument is the color, in one of four shapes told apart by structure. The optional second argument is a config override and works with every shape.
| Shape | Recognized by | Example | Form |
|---|---|---|---|
| Bare string | A string | '#26fcb2', 'oklch(0.85 0.18 152)' |
Value |
| Value object | Any other object | { h: 152, s: 0.95, l: 0.74 }, { r: 38, g: 252, b: 178 } |
Value |
{ from, ...overrides } |
Has a from key |
{ from: '#1a1a2e', base: bg, contrast: 'AA' } |
Value |
| Structured | Has a hue key |
{ hue: 152, saturation: 95, tone: 74 } |
Structured |
// bare string
glaze.color('#26fcb2');
// OKHSL value object
glaze.color({ h: 152, s: 0.95, l: 0.74 });
// OKHST value object (tone axis)
glaze.color({ h: 152, s: 0.95, t: 0.7 });
// value + overrides
glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' });
// structured, 0–100 scales
glaze.color({ hue: 152, saturation: 95, tone: 74 });The three value forms start from a literal color and take GlazeColorOverrides. The structured form is a theme-style definition on 0–100 scales; see GlazeColorInput.
type GlazeColorValue =
| string
| OkhslColor
| OkhstColor
| RgbColor
| OklchColor;
// An editor hint; strings are validated at runtime.
type HexColor = `#${string}`;
interface OkhslColor { h: number; s: number; l: number }
interface OkhstColor { h: number; s: number; t: number }
interface RgbColor { r: number; g: number; b: number }
interface OklchColor { l: number; c: number; h: number }The literal color forms. They are accepted by glaze.color(), by from on a theme color, by a token’s base, and by glaze.shadow() / glaze.glow().
| Form | Example | Notes |
|---|---|---|
| Hex | '#26fcb2', '#abc', '#26fcb2ff', '#abcd' |
3, 4, 6, or 8 digits after #. |
rgb() / rgba() |
'rgb(38 252 178)', 'rgba(38, 252, 178, 0.8)' |
Channels 0–255 or percentages. |
hsl() / hsla() |
'hsl(152 97% 57%)' |
Saturation and lightness as percentages, or bare 0–1 fractions. Unlike CSS, a bare hsl(120 50 50) is not 50%: it is out of range and resolves to white, without a warning. |
okhsl() |
'okhsl(152 95% 74%)' |
OKHSL, Glaze’s own output format. Saturation and lightness as percentages or bare 0–1 fractions, as for hsl(). |
okhst() |
'okhst(152 95% 70%)' |
OKHST: the third value is tone (70% = tone 70). Percentages or bare 0–1 fractions, as for hsl(). Not a native CSS color; Tasty output can carry it. |
oklch() |
'oklch(0.85 0.18 152)' |
L 0–1 (or a percentage), C (a percentage maps 100% to 0.4), H in degrees. |
OkhslColor |
{ h: 152, s: 0.95, l: 0.74 } |
h 0–360; s, l 0–1. |
OkhstColor |
{ h: 152, s: 0.95, t: 0.7 } |
h 0–360; s, t 0–1. The t key tells it apart from OKHSL. |
RgbColor |
{ r: 38, g: 252, b: 178 } |
sRGB, 0–255. |
OklchColor |
{ l: 0.85, c: 0.18, h: 152 } |
Same meaning as oklch(): L and C 0–1, H in degrees. |
-
Function names are case-insensitive. Components can be separated by spaces or commas.
-
An alpha component (
#rrggbbaa,#rgba,/ 0.8, or any fourth component, comma- or space-separated) is dropped with aconsole.warn. Useopacityinstead:glaze: alpha component dropped from "rgb(1 2 3 4)"; use the color's `opacity` (or a transparent mix) for translucency. -
Value objects are recognized by their keys:
r,g,b→ RGB;l,c,h→ OKLCh;h,s,t→ OKHST; otherwise OKHSL. -
A hue is a bare number (degrees) or takes
deg,turn,rad, orgrad:hsl(0.5turn 50% 50%)has hue 180°. -
A component may be the CSS keyword
none, read as 0 as CSS renders a missing component:oklch(0.97 0 none)is a gray. Any other component that isn’t a number throws (glaze: invalid component "abc" in "hsl(abc 50% 50%)".). -
Out-of-range values clamp, as CSS clamps them: an
rgb()channel to 0–255,hsl()/okhsl()/okhst()percentages to 0–100%,oklch()L to 0–1 and C to at least 0. A value object clamps a 0–1 field between 1 and 1.5 the same way, but throws for one below 0 or above 1.5, and for an RGB channel outside 0–255 (below). -
Not supported: named colors (
'red'), relative color syntax, and[r, g, b]tuples.
| Input | Error |
|---|---|
'red' |
glaze: unsupported color string "red". |
'#12' |
glaze: invalid hex color "#12". |
'rgb(38 252)' |
glaze: expected 3 components in "rgb(38 252)". |
'rgb(38 252 x)' |
glaze: invalid component "x" in "rgb(38 252 x)". |
{ h: 152, s: 95, l: 74 } |
glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)? |
{ h: 152, s: -0.1, l: 0.74 } |
glaze.color: OkhslColor s must be 0 or more (got -0.1). |
[38, 252, 178] |
glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead. |
{ r: 300, g: 0, b: 0 } |
glaze.color: RgbColor r must be a finite number in 0–255 (got 300). |
The OKHSL, OKHST, and OKLCh object checks reject s, l, t, or c below 0, and above 1.5, which catches 0–100 input meant for the structured form. String components have no such guard (see hsl() above).
The fields a value form can carry next to its color. GlazeFromInput is GlazeColorOverrides plus a required from: GlazeColorValue; a bare string or value object is the same with no overrides.
| Field | Type | Default | Description |
|---|---|---|---|
hue |
number | RelativeValue |
The value’s hue | Absolute (0–360) replaces the hue. '+N' / '-N' is relative to the value’s own hue, never to base. |
saturation |
number |
The value’s saturation | Saturation seed, 0–100. Replaces the value’s saturation. |
saturationFactor |
number |
1 |
Multiplier (0–1) on the saturation seed. |
tone |
HCPair<ToneValue> |
The value’s tone | Absolute (0–100), a tone delta ('+N' / '-N'), or 'max' / 'min'. A delta is measured from the value’s own tone, or, with base, from the base. Under mode: 'auto' the dark variants mirror it; see Pairing colors. |
darkHue |
number | RelativeValue |
hue |
Hue for the dark variants. Absolute, or relative to the seed hue (the value’s hue, or an absolute hue override). |
darkSaturation |
number |
saturation |
Dark saturation seed, 0–100. When set, darkDesaturation is not applied on top. |
darkSaturationFactor |
number |
The light factor | Dark multiplier (0–1) on the dark seed. When set, darkDesaturation is not applied on top. |
mode |
AdaptationMode |
'auto' |
'fixed' maps without inverting; 'static' keeps the same tone in every variant. See mode. |
autoFlip |
boolean |
The config’s autoFlip |
Mirror out-of-range results instead of clamping. See autoFlip. |
contrast |
HCPair<ContrastSpec> |
— | Contrast floor. Without base, measured against the input value itself; with base, against the base in each scheme. See Contrast and high contrast. |
base |
GlazeColorToken | GlazeColorValue |
— | The color this token’s contrast and tone delta are measured against. See Pairing colors. |
opacity |
number |
1 |
Fixed alpha, 0–1. Out of range throws. With contrast, emits a console.warn. |
pastel |
boolean |
The config override’s pastel (false) |
See Per-color pastel. |
role |
RoleInput |
'text' |
Fixes APCA polarity. See Roles on tokens. |
name |
string |
— | Debug label. See name. |
// hue 282.9 → 302.9
glaze.color({ from: '#1a1a2e', hue: '+20' });
// saturation 0.375 → 0.5
glaze.color({ from: '#1a1a2e', saturation: 50 });
// saturation 0.375 → 0.187
glaze.color({ from: '#1a1a2e', saturationFactor: 0.5 });The structured form: the same definition a theme color would have, with its own seed. Hue is in degrees; saturation and tone are on 0–100.
| Field | Type | Default | Description |
|---|---|---|---|
hue |
number |
— | Hue seed, 0–360. Required. |
saturation |
number |
— | Saturation seed, 0–100. Required. |
tone |
HCPair<number | ExtremeValue> |
— | Tone 0–100, or 'max' / 'min'. Required. Tone deltas aren’t accepted. |
saturationFactor |
number |
1 |
Multiplier (0–1) on the seed. |
darkHue |
number | RelativeValue |
hue |
Dark hue: absolute, or relative to hue. |
darkSaturation |
number |
saturation |
Dark saturation seed, 0–100. |
darkSaturationFactor |
number |
The light factor | Dark multiplier (0–1) on the dark seed. |
mode |
AdaptationMode |
'auto' |
Adaptation mode. |
autoFlip |
boolean |
The config’s autoFlip |
Mirror out-of-range results instead of clamping. |
opacity |
number |
1 |
Fixed alpha, 0–1. |
base |
GlazeColorToken | GlazeColorValue |
— | See Pairing colors. |
contrast |
HCPair<ContrastSpec> |
— | Without base, measured against the token’s own input color (hue, saturation, normal tone). |
pastel |
boolean |
The config override’s pastel (false) |
Per-color pastel. |
role |
RoleInput |
'text' |
Fixes APCA polarity. See Roles on tokens. |
name |
string |
— | Debug label. |
Out-of-range numbers throw before anything resolves, for example glaze.color: structured saturation must be a finite number in 0–100 (got 120). or glaze.color: structured tone must be a finite number in 0–100 or 'max'/'min' (got +5). hue and darkHue must be finite; the saturation seeds 0–100; the factors and opacity 0–1.
- Every form defaults to
mode: 'auto', so a token adapts between light and dark like a theme color. - Value forms (bare strings, value objects, and
{ from, ... }) keep the input tone exactly in the light variant: they resolve with a locallightTone: false. PasslightTonein the config override to change that. - The structured form uses the live global tone windows, like a theme color.
- Every other config field follows the live global config at resolve time, unless the config override sets it.
pastelis instance-only: it comes from the override or the color’spastel, never fromglaze.configure().
const brand = glaze.color('#26fcb2');
brand.resolve().light; // renders exactly #26fcb2
brand.json();
// → {
// light: 'oklch(0.8789 0.1857 162.47)',
// dark: 'oklch(0.3446 0.066 162.47)',
// }| Method | Returns | Description |
|---|---|---|
token.resolve() |
ResolvedColor |
The four variants. name is the name, or 'value' without one. |
token.token(options?) |
Record<string, string> |
One color’s state map, keyed by state alias: '', the dark state, the high-contrast state, and the two combined. Options: format, modes, states. |
token.tasty(options?) |
Record<string, string> |
The same map as token.token(), for Tasty. |
token.json(options?) |
Record<string, string> |
{ light, dark, lightContrast?, darkContrast? }. Options: format, modes. |
token.css(options) |
GlazeCssResult |
CSS custom property declarations, one string per variant. Always all four. Options: GlazeColorCssOptions. |
token.dtcg(options?) |
GlazeColorDtcgResult |
One W3C DTCG token ({ $type: 'color', $value }) per variant, under light, dark?, lightContrast?, darkContrast?. Options: colorSpace ('srgb' default, or 'oklch'), modes. |
token.dtcgResolver(options) |
GlazeDtcgResolverDocument |
One DTCG Resolver-Module document with the color under name. Options: name (required) plus the theme.dtcgResolver() options. |
token.tailwind(options) |
string |
A Tailwind v4 @theme block for --color-<name> plus dark and high-contrast overrides. Options: name (required) plus the theme.tailwind() options. |
token.export(override?) |
GlazeColorTokenExport |
JSON-safe authoring snapshot. See Snapshots. |
const brand = glaze.color('#26fcb2');
brand.token();
// → {
// '': 'oklch(0.8789 0.1857 162.47)',
// '@media(prefers-color-scheme: dark)': 'oklch(0.3446 0.066 162.47)',
// }
brand.css({ name: 'brand' }).light;
// → '--brand-color: oklch(0.8789 0.1857 162.47);'
brand.tailwind({ name: 'brand' });
// → '@theme {\n' +
// ' --color-brand: oklch(0.8789 0.1857 162.47);\n' +
// '}\n\n.dark {\n' +
// ' --color-brand: oklch(0.3446 0.066 162.47);\n' +
// '}'- Every output that takes
formatdefaults to'oklch';dtcg()anddtcgResolver()take acolorSpaceinstead ('srgb'by default).json(),css(), andtailwind()accept only the native CSS formats ('oklch','rgb','hsl') and throw on'okhsl'/'okhst';token()andtasty()accept all five. See Color formats. modesandstatesdefault to the global config, as on themes.css()ignoresmodes. See Output modes.- A token is never a glow, so it never carries an HDR value:
modes.hdr,states.hdr, andhdrSelectorchange nothing, andcss()has nohdroption. - The output methods share their option types with themes, except four.
token()andtasty()takeGlazeTokenOptions—theme.tasty()’s options withoutsplitHueandname.css()takes theGlazeColorCssOptionsbelow.tailwind()anddtcgResolver()takeGlazeColorTailwindOptionsandGlazeColorDtcgResolverOptions: the theme types plus a requiredname. Full shapes are in Output formats.
| Field | Type | Default | Description |
|---|---|---|---|
name |
string |
— | Required. The custom property name without --: 'brand' gives --brand-color. |
format |
GlazeColorFormat |
'oklch' |
Native formats only. |
suffix |
string |
'-color' |
Appended to name, matching theme.css(). With splitHue, '-hue' throws (below). |
splitHue |
boolean |
false |
Emit the hue as a separate --<name>-hue property referenced with var(), rounded to 2 decimals like the hue in oklch() output. Requires format: 'oklch': any other format throws. Throws when the token isn’t pastel. See splitHue. |
const brand = glaze.color('#26fcb2', { pastel: true });
brand.css({ name: 'brand', splitHue: true }).light;
// → '--brand-hue: 162.47;\n' +
// '--brand-color: oklch(0.8789 0.0572 var(--brand-hue));'The hue var and the value property share one namespace, so suffix: '-hue' would put both on --brand-hue. That throws:
glaze: splitHue would declare "--brand-hue" twice: for the hue of token
"brand" and for the value of token "brand".
One would silently override the other.
Pass a `suffix` other than '-hue'.A token achromatic in light declares no hue var, so '-hue' is accepted there: glaze.color('#808080', { pastel: true }) with name: 'gray' gives --gray-hue: oklch(0.5999 0 0);.
The second argument of glaze.color() is a GlazeConfigOverride, the same type a theme takes: the resolve-relevant subset of the global config plus the instance-only pastel. The fields are listed in GlazeConfigOverride. It is sparse: fields it sets are fixed for the token, and fields it omits follow the live global config at resolve time; the details are in Merge rules.
On a token specifically:
- The override is copied when the token is created, so changing the object you passed afterwards has no effect. Its fields are read by property access, so a class instance with getters or a proxy works.
- Value forms resolve with a local
lightTone: false; alightTonein the override replaces it (see Defaults). shadowTuningandglowTuninghave no effect: a token is never a shadow or a glow. They are still validated at resolve, so aheadroomthat isn’t a finite number ≥ 0, or anyshadowTuning.headroom, throws.inferRoleis accepted but has no effect: a token never infers its role fromname(see Roles on tokens).- A
contrastLevelmoves only this token. On a token with abase, set it on both; see Clearing it, exporting it, base links. modesandstatesaren’t part of the override. Pass them to the output methods or set them globally.
// Let black flip all the way to white in dark.
glaze.color('#000000', { lightTone: false, darkTone: [15, 100] });
// dark: #ffffff (with the default darkTone, #f1f1f1)
// The structured form takes an override too.
glaze.color(
{ hue: 152, saturation: 95, tone: 74 },
{ darkTone: false, pastel: true },
);Set base to anchor a token to another token or a raw color value. contrast and a tone delta then measure against the base’s resolved variant in each scheme instead of against the token’s own input, so text keeps its floor against its background in light, dark, and high contrast.
const bg = glaze.color('#1a1a2e');
// AA against bg in every scheme.
const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
// light: #ffffff on #1a1a2e (17.06:1); dark: #222222 on #dadbec (11.58:1)
// 8 tones lighter than bg, except in darkContrast: bg sits at tone 93
// there, so autoFlip mirrors the step to 8 tones darker.
const border = glaze.color({
from: '#000000',
base: bg,
tone: '+8',
mode: 'fixed',
});
// A raw value is wrapped with glaze.color(value), so this equals `text`.
const text2 = glaze.color({
from: '#ffffff',
base: '#1a1a2e',
contrast: 'AA',
});With base:
contrastis solved against the base’slight,dark,lightContrast, anddarkContrastvariant in turn: what the base token paints, under its own config, whatever form it was made in.- A tone delta (
'+N'/'-N') is measured from the base, as for a theme’s dependent colors. In light and high contrast the step is exact: a'+0'lands on the base. Undermode: 'auto'the dark variant steps from where the base sits on its own authored scale — its authored tone, read under the base’s ownlightTone, or, for a'fixed'or'static'base or an'auto'base placed by a contrast floor or an extreme, its own dark tone read back through the dark window, as in a theme — then inverts and maps the result through the dark window, so the step flips direction and shrinks to the dark window’s scale (about 0.87×), as in a theme: an'auto'version ofborderabove is 6.93 tones darker thanbgin dark.'fixed'and'static'apply it to the base’s tone in each scheme, sign kept. - A base whose active dark window differs from the dependent’s — its own
darkTone, or anothercontrastLevel— is read back through the dependent’s dark window instead, so a'+0'dependent lands on it and steps mirror at the dependent’s scale. Onglaze.color('#808080', { darkTone: false }),'+6'steps 5.2 tones darker in dark and'-6'5.2 lighter. - A relative
huestill anchors to the token’s own seed (the value passed toglaze.color()), not to the base. modeapplies to the pair:'auto'mirrors the relationship in dark,'fixed'keeps its sign.- The base is resolved lazily, when the dependent first resolves.
- An unreachable floor pins the token to the extreme of the side the solver searched first and emits a
console.warn, such asglaze: color "value" cannot meet WCAG 4.50 in dark scheme (got <ratio>). …, deduplicated by color, scheme, target, and measured value. Setnameto make the warning identifiable.
Chains compose:
const page = glaze.color('#000000');
const surface = glaze.color({
from: '#222222',
base: page,
contrast: 'AAA',
});
const label = glaze.color({
from: '#ffffff',
base: surface,
contrast: 'AA',
});A token’s role is its explicit role, or 'text'. The token is the foreground and its anchor is the background, whichever anchor it has:
- With a
base, the base is the background. - Without a
base, acontrastfloor or tone delta is measured against a hidden copy of the token’s own input color, and that copy is the background.
// Text on white: light is rgb(141 141 141), Lc 60.5. Identical to the
// same token with base: glaze.color({ from: '#ffffff', mode: 'static' }).
glaze.color({ from: '#ffffff', contrast: { apca: 60 } });
// A fill with white on top: light is rgb(152 152 152), Lc 60.5 the
// other way.
glaze.color({ from: '#ffffff', contrast: { apca: 60 }, role: 'surface' });Set role: 'surface' (or an alias such as 'bg') for a token that is a fill with its anchor drawn on top. Unlike a theme color, a token never infers its role from name, so inferRole has no effect on it. WCAG ignores the role.
name is a debug label. It appears in warnings and errors instead of "value", and as resolve().name. It doesn’t change output keys: token(), tasty(), and json() stay keyed by scheme, and the custom property name comes from css({ name }). It doesn’t change the role either: name: 'card-bg' is still measured as text unless you set role.
Invalid name |
Error |
|---|---|
'' or whitespace |
glaze.color: name must be a non-empty string. Omit `name` if you do not want to set a debug label. |
'value', 'seed', 'externalBase' |
glaze.color: name "value" is reserved (used internally). Reserved names are: "value", "seed", "externalBase". Pick a different name. |
token.export() returns a detached, JSON-safe authoring snapshot: the original input, the overrides (with the base exported recursively), and the effective config frozen at call time. glaze.colorFrom() rebuilds the token from it and pins the frozen config, so later glaze.configure() calls don’t change the restored token.
const bg = glaze.color('#1a1a2e');
const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
const data = text.export();
// → {
// kind: 'color',
// version: 2,
// form: 'value',
// input: '#ffffff',
// overrides: {
// contrast: 'AA',
// base: {
// kind: 'color',
// version: 2,
// form: 'value',
// input: '#1a1a2e',
// config: { … },
// },
// },
// config: {
// lightTone: false,
// darkTone: { lo: 15, hi: 95, eps: 0.05 },
// darkDesaturation: 0.1,
// autoFlip: true,
// pastel: false,
// inferRole: true,
// },
// }
const restored = glaze.colorFrom(JSON.parse(JSON.stringify(data)));
// restored.resolve() is identical to text.resolve()- Both the value and the structured forms round-trip.
- An explicit
role, an alias such as'bg'included, is recorded as written — inoverrides.role(value form) orinput.role(structured form) — and restored byglaze.colorFrom(). A token withoutroleisn’t frozen to one; it restores with the'text'default. overridemerges over the token’s own override for the snapshot only, and is passed to a nestedbaseexport too.- The config freeze follows the same rules as
theme.export():contrastLevelis frozen only when set on the token or passed asoverride;modesandstatesnever are.
GlazeColorTokenExport:
| Field | Type | Description |
|---|---|---|
kind |
'color' |
Always written; optional on legacy snapshots. |
version |
number |
GLAZE_EXPORT_VERSION (2). Always written; optional on legacy snapshots. |
form |
'value' | 'structured' |
Which input shape created the token. |
input |
GlazeColorValue | GlazeColorInputExport |
The raw value (form: 'value') or the structured input. |
overrides |
GlazeColorOverridesExport |
The value form’s overrides, when any were given. |
config |
GlazeConfigOverride |
The frozen effective config. |
GlazeColorInputExport and GlazeColorOverridesExport have the same fields as GlazeColorInput and GlazeColorOverrides, except that a base is stored as its GlazeColorTokenExport: a raw-value base as the snapshot of the token it is wrapped in, so its config is frozen too. A snapshot written by an earlier release with a raw-value base still restores, with that base following the live config.
glaze.colorFrom() throws when:
| Condition | Error |
|---|---|
data isn’t an object |
glaze.colorFrom: expected an object from token.export(), got <type>. |
kind is present and not 'color' |
glaze.colorFrom: expected kind "color", got "<kind>". |
version is invalid or newer than the library |
See Restore checks. |
form isn’t 'value' or 'structured' |
glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got <form>). |
input is missing |
glaze.colorFrom: missing "input" field — expected the original GlazeColorValue. (or GlazeColorInput) |
glaze.isColorTokenExport(data) tells a token snapshot apart from theme and palette snapshots; see Type guards.