Skip to content
Glazev3.1.0
Color scheme
Contrast
Color scheme
Contrast

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, config?)

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.

GlazeColorValue

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 a console.warn. Use opacity instead:

    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, or grad: 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).

GlazeColorOverrides

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 });

GlazeColorInput

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.

Defaults

  • 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 local lightTone: false. Pass lightTone in 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. pastel is instance-only: it comes from the override or the color’s pastel, never from glaze.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)',
// }

Token methods

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 format defaults to 'oklch'; dtcg() and dtcgResolver() take a colorSpace instead ('srgb' by default). json(), css(), and tailwind() accept only the native CSS formats ('oklch', 'rgb', 'hsl') and throw on 'okhsl' / 'okhst'; token() and tasty() accept all five. See Color formats.
  • modes and states default to the global config, as on themes. css() ignores modes. See Output modes.
  • A token is never a glow, so it never carries an HDR value: modes.hdr, states.hdr, and hdrSelector change nothing, and css() has no hdr option.
  • The output methods share their option types with themes, except four. token() and tasty() take GlazeTokenOptions — theme.tasty()’s options without splitHue and name. css() takes the GlazeColorCssOptions below. tailwind() and dtcgResolver() take GlazeColorTailwindOptions and GlazeColorDtcgResolverOptions: the theme types plus a required name. Full shapes are in Output formats.

GlazeColorCssOptions

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);.

Config overrides

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; a lightTone in the override replaces it (see Defaults).
  • shadowTuning and glowTuning have no effect: a token is never a shadow or a glow. They are still validated at resolve, so a headroom that isn’t a finite number ≥ 0, or any shadowTuning.headroom, throws.
  • inferRole is accepted but has no effect: a token never infers its role from name (see Roles on tokens).
  • A contrastLevel moves only this token. On a token with a base, set it on both; see Clearing it, exporting it, base links.
  • modes and states aren’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 },
);

Pairing colors

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:

  • contrast is solved against the base’s light, dark, lightContrast, and darkContrast variant 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. Under mode: 'auto' the dark variant steps from where the base sits on its own authored scale — its authored tone, read under the base’s own lightTone, 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 of border above is 6.93 tones darker than bg in 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 another contrastLevel — 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. On glaze.color('#808080', { darkTone: false }), '+6' steps 5.2 tones darker in dark and '-6' 5.2 lighter.
  • A relative hue still anchors to the token’s own seed (the value passed to glaze.color()), not to the base.
  • mode applies 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 as glaze: color "value" cannot meet WCAG 4.50 in dark scheme (got <ratio>). …, deduplicated by color, scheme, target, and measured value. Set name to 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',
});

Roles on tokens

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, a contrast floor 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

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(override?) / glaze.colorFrom(data)

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 — in overrides.role (value form) or input.role (structured form) — and restored by glaze.colorFrom(). A token without role isn’t frozen to one; it restores with the 'text' default.
  • override merges over the token’s own override for the snapshot only, and is passed to a nested base export too.
  • The config freeze follows the same rules as theme.export(): contrastLevel is frozen only when set on the token or passed as override; modes and states never 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.