Skip to content

Latest commit

 

History

History
2021 lines (1581 loc) · 131 KB

File metadata and controls

2021 lines (1581 loc) · 131 KB

API Reference

Full reference for every public method, option, and type exported by @tenphi/glaze. Organized for lookup, not for reading top-to-bottom — see methodology.md for a guided walkthrough of how to use these primitives to build a real palette.

Contents


Theme creation

Method Description
glaze(hue, saturation?, config?) Create a theme from hue (0–360) and saturation (0–100). Optional config overrides the global config for this theme.
glaze({ hue, saturation, darkHue?, darkSaturation? }, config?) Create a theme from an options object, with optional per-theme config override. darkHue / darkSaturation seed the dark schemes — see Dark seed.
glaze.themeFrom(data) Create a theme from a theme.export() snapshot (kind: 'theme').
glaze.from(data) Compat alias for glaze.themeFrom.
glaze.fromHex(hex) Create a theme from a hex color (#rgb or #rrggbb). Extracts hue and saturation.
glaze.fromRgb(r, g, b) Create a theme from RGB values (0–255). Extracts hue and saturation.
glaze.paletteFrom(data) Create a palette from a palette.export() snapshot (kind: 'palette').
glaze.colorFrom(data) Create a color token from a token.export() snapshot (kind: 'color').
glaze.isThemeExport(data) Type guard for theme authoring snapshots.
glaze.isColorTokenExport(data) Type guard for color-token authoring snapshots.
glaze.isPaletteExport(data) Type guard for palette authoring snapshots.
const a = glaze(280, 80);
const b = glaze({ hue: 280, saturation: 80 });
const bDark = glaze({ hue: 280, saturation: 80, darkHue: 268, darkSaturation: 65 });
const c = glaze.fromHex('#7a4dbf');
const d = glaze.fromRgb(122, 77, 191);
const e = glaze.themeFrom(a.export());

// Per-theme config override:
const rawTheme = glaze(280, 80, { lightTone: false, darkTone: false });

Authoring restore triad (parallel to .export() on each instance):

Export Restore
theme.export() glaze.themeFrom()
token.export() glaze.colorFrom()
palette.export() glaze.paletteFrom()

Every snapshot includes kind + version (GLAZE_EXPORT_VERSION, currently 1). Legacy snapshots without those fields still restore. Wrong kind, or a version outside 1..=GLAZE_EXPORT_VERSION, throws.

The optional config parameter is a GlazeConfigOverride — see Per-instance config override.


Theme methods

A GlazeTheme exposes:

Method Description
theme.hue (readonly) The hue seed (0–360).
theme.saturation (readonly) The saturation seed (0–100).
theme.darkHue (readonly) The dark-scheme hue seed (0–360), or undefined when hue is reused.
theme.darkSaturation (readonly) The dark-scheme saturation seed (0–100), or undefined when saturation is reused.
theme.colors(defs) Add/replace colors (additive merge — adds new, overwrites existing by name, doesn't remove others).
theme.color(name) Get a color definition by name.
theme.color(name, def) Set a single color definition.
theme.remove(name | names[]) Remove one or more color definitions.
theme.has(name) Check if a color is defined.
theme.list() List all defined color names.
theme.reset() Clear all color definitions.
theme.export(override?) Export the theme configuration as a JSON-safe object (optional config override at export time).
theme.extend(options) Create a child theme inheriting all color definitions (see extend below).
theme.resolve() Resolve all colors and return a Map<string, ResolvedColor>.
theme.tokens(options?) Export as a flat token map grouped by scheme variant.
theme.tasty(options?) Export as Tasty style-to-state bindings.
theme.json(options?) Export as plain JSON.
theme.css(options?) Export as CSS custom property declarations.
theme.dtcg(options?) Export one W3C DTCG token tree per scheme.
theme.dtcgResolver(options?) Export one DTCG Resolver-Module document containing every scheme.
theme.tailwind(options?) Export a Tailwind CSS v4 theme and scheme overrides.

theme.colors(defs)

theme.colors({ surface: { tone: 97 } });
theme.colors({ text: { tone: 30 } });
// Both 'surface' and 'text' are now defined.

theme.color(name) / theme.color(name, def)

theme.color('surface', { tone: 97, saturation: 0.75 }); // set
const def = theme.color('surface'); // get

theme.extend(options)

Creates a new theme inheriting all color definitions, optionally replacing the hue / saturation seed, color overrides, and config:

const danger = primary.extend({
  hue: 23,
  colors: { 'accent-fill': { tone: 48, mode: 'fixed' } },
});

// Inherit parent's config override and widen the dark window further:
const highSat = base.extend({ config: { darkTone: [10, 100] } });

GlazeExtendOptions:

Field Type Description
hue number Replace the hue seed. Defaults to the parent's hue.
saturation number Replace the saturation seed. Defaults to the parent's saturation.
darkHue number Replace the dark hue seed. Defaults to the parent's darkHue.
darkSaturation number Replace the dark saturation seed. Defaults to the parent's darkSaturation.
colors ColorMap Per-theme overrides (additive merge over the inherited map).
config GlazeConfigOverride Config override for the child. Shallow-merged with the parent's override — child fields win.

Colors marked with inherit: false on the parent are not copied into the child.

theme.resolve()

Resolves the dependency graph and returns a Map<string, ResolvedColor>. Export methods call it automatically; use it directly for tests, diagnostics, or a custom output pipeline.

const resolved = theme.resolve();
const surface = resolved.get('surface');
// surface?.light.t is canonical tone on 0–1.

theme.tokens(options?)

Flat token map grouped by scheme variant.

theme.tokens();
// → { light: { surface: 'oklch(...)' }, dark: { surface: 'oklch(...)' } }

GlazeJsonOptions:

Option Default Description
format 'oklch' Output color format. One of 'rgb' | 'hsl' | 'oklch'. 'okhsl' and 'okhst' throw — use tasty() for those.
modes { dark: true, highContrast: false } (or global config) Which scheme variants to include.

theme.tasty(options?)

Style-to-state bindings for the Tasty style system. Uses #name color token keys and state aliases. By default the dark and high-contrast variants are keyed by media-query states ('@media(prefers-color-scheme: dark)', '@media(prefers-contrast: more)') so tokens work without registering custom states.

theme.tasty();
// → {
//   '#surface': { '': 'oklch(...)', '@media(prefers-color-scheme: dark)': 'oklch(...)' },
//   ...
// }

GlazeTokenOptions:

Option Default Description
format 'oklch' Output color format. 'okhsl' and 'okhst' are also supported here (Tasty-only spaces).
modes global config Which scheme variants to include.
states.dark '@media(prefers-color-scheme: dark)' (or global config) State alias for dark mode tokens.
states.highContrast '@media(prefers-contrast: more)' (or global config) State alias for high-contrast tokens.
splitHue false Emit hue as a separate custom property ($name-hue token + var() in oklch values). Requires format: 'oklch' and every color to be pastel.
name 'theme' Base name for the theme-level hue var ($theme-hue / --theme-hue). Palette export auto-derives this from the theme name.
prefix (palette only) See Palette.

When both dark and highContrast modes are enabled, dark high-contrast variants are emitted under the combined key <dark> & <highContrast> (e.g. '@media(prefers-color-scheme: dark) & @media(prefers-contrast: more)').

theme.json(options?)

Per-color JSON map.

theme.json();
// → {
//   surface: { light: 'oklch(...)', dark: 'oklch(...)' },
//   text:    { light: 'oklch(...)', dark: 'oklch(...)' },
// }

Same options as tokens().

theme.css(options?)

CSS custom property declaration strings, grouped by scheme variant.

theme.css();
// → {
//   light: '--surface-color: oklch(...);\n--text-color: oklch(...);',
//   dark:  '--surface-color: oklch(...);\n--text-color: oklch(...);',
//   lightContrast: '...',
//   darkContrast:  '...',
// }

GlazeCssOptions:

Option Default Description
format 'oklch' Output color format. One of 'rgb' | 'hsl' | 'oklch'. 'okhsl' and 'okhst' throw — use tasty() for those.
suffix '-color' Suffix appended to each CSS property name. Pass '' for bare property names.
splitHue false Emit hue as a separate --*-hue custom property referenced via var() in oklch color values. Requires format: 'oklch' and every color to be pastel. Shadow/mix colors stay inline (blended hue; they do not follow --hue rotation).
name 'theme' Base name for the theme-level hue var (--theme-hue). Palette export auto-derives this from the theme name.

GlazeCssResult always contains all four keys (light, dark, lightContrast, darkContrast); empty if no colors are defined for that variant.

theme.dtcg(options?)

W3C Design Tokens Format Module (2025.10) documents — the vendor-neutral JSON format consumed by Figma, Tokens Studio, Style Dictionary v4+, Terrazzo, Penpot, and every DTCG-compatible tool. Returns one spec-conformant token tree per scheme variant.

theme.dtcg();
// → {
//   light: {
//     surface: {
//       $type: 'color',
//       $value: { colorSpace: 'srgb', components: [0.96, 0.94, 0.98], hex: '#f5f0fa' },
//     },
//   },
//   dark: {
//     surface: {
//       $type: 'color',
//       $value: { colorSpace: 'srgb', components: [0.16, 0.14, 0.2], hex: '#292333' },
//     },
//   },
// }

Write each document to its own .tokens.json file — one file per scheme is the most tool-compatible convention (one per Style Dictionary theme / Tokens Studio set / Figma variable mode).

GlazeDtcgOptions:

Option Default Description
colorSpace 'srgb' Color space for $value. 'srgb' emits gamma sRGB components (0–1) plus a hex hint — universally understood. 'oklch' emits [L, C, H] components with no hex — Glaze-native, wide-gamut.
modes global config Which scheme variants to include. light is always present.

alpha is included on $value only when the color's opacity is below 1. $type is always 'color'.

theme.dtcgResolver(options?)

A single W3C DTCG Resolver-Module document describing every scheme variant in one file — an alternative to dtcg()'s per-scheme files for tools that resolve sets + modifiers (e.g. Dispersa). The light document becomes sets.base.sources[0] (the default context); each other variant becomes a context override on a single scheme modifier.

theme.dtcgResolver({ modes: { highContrast: true } });
// → {
//   version: '2025.10',
//   sets: {
//     base: {
//       sources: [
//         {
//           surface: {
//             $type: 'color',
//             $value: { colorSpace: 'srgb', components: [0.96, 0.94, 0.98], hex: '#f5f0fa' },
//           },
//         },
//       ],
//     },
//   },
//   modifiers: {
//     scheme: {
//       default: 'light',
//       contexts: {
//         light: [],
//         dark: [
//           {
//             surface: {
//               $type: 'color',
//               $value: { colorSpace: 'srgb', components: [0.16, 0.14, 0.2], hex: '#292333' },
//             },
//           },
//         ],
//         lightContrast: [ /* … */ ],
//         darkContrast: [ /* … */ ],
//       },
//     },
//   },
//   resolutionOrder: [
//     { $ref: '#/sets/base' },
//     { $ref: '#/modifiers/scheme' },
//   ],
// }

Why one modifier with four contexts. Glaze resolves darkContrast independently — it is not dark + lightContrast layered. The resolver model composes modifiers additively (last in resolutionOrder wins on conflict), so two independent modifiers (scheme × contrast) would produce wrong values for the dark + high-contrast permutation. One scheme modifier with a context per variant keeps every resolved value exact. Choose dtcgResolver() when you want single-file theming and feed it to a resolver tool; choose dtcg() for maximum per-file tool compatibility.

GlazeDtcgResolverOptions (extends GlazeDtcgOptions, so modes and colorSpace pass through):

Option Default Description
colorSpace 'srgb' Same as dtcg() — flows through to every source and context.
modes global config Which scheme variants to emit as contexts. light is always present (the default); absent variants are omitted.
setName 'base' Name of the single set holding the default (light) token tree.
modifierName 'scheme' Name of the modifier describing the scheme axis.
contextNames identity Override the four context names (light / dark / lightContrast / darkContrast) — e.g. { dark: 'night' }.
version '2025.10' Resolver document version.

theme.tailwind(options?)

A Tailwind CSS v4 @theme block (light baseline) plus dark / high-contrast overrides under configurable selectors. Returns a single ready-to-paste CSS string. The --color-* namespace auto-generates bg-* / text-* / border-* utilities.

@theme {
  --color-surface: oklch(0.96 0.01 280);
  --color-text: oklch(0.3 0.05 280);
}
.dark {
  --color-surface: oklch(0.16 0.01 280);
  --color-text: oklch(0.85 0.05 280);
}
.high-contrast {
  --color-surface: oklch(0.98 0.01 280);
  --color-text: oklch(0.1 0.05 280);
}
.dark.high-contrast {
  --color-surface: oklch(0.05 0.01 280);
  --color-text: oklch(0.95 0.05 280);
}

GlazeTailwindOptions:

Option Default Description
format 'oklch' Output color format for the values.
namespace 'color-' CSS custom property namespace, forming --<namespace><name> (e.g. --color-surface). Named namespace to avoid clashing with the palette theme-prefix option.
darkSelector '.dark' Selector wrapping the dark overrides. Pass an at-rule like '@media (prefers-color-scheme: dark)' to drive dark mode from the OS preference (it nests :root automatically).
highContrastSelector '.high-contrast' Selector wrapping the light high-contrast overrides. The combined dark + high-contrast block uses ${darkSelector}${highContrastSelector} (e.g. .dark.high-contrast).
modes global config Which scheme variants to include. The @theme block (light) is always emitted when colors exist.

theme.export(override?)

const snapshot = theme.export();
// → {
//     kind: 'theme',
//     version: 1,
//     hue: 280,
//     saturation: 80,
//     colors: { surface: { ... }, ... },
//     config: { lightTone: {...}, darkTone: {...}, pastel: false, ... },
//   }

const restored = glaze.themeFrom(snapshot);

Returns a deep-cloned, JSON-safe authoring snapshot (definitions + frozen effective config — not resolved color strings). Freezes getConfig() ∪ instance local ∪ override at call time. Restored themes pin that freeze as their local override. Distinct from theme.json(), which emits resolved color strings.


High-contrast pairs

HCPair<T> means either one value used in both ordinary and high-contrast schemes, or an explicit [normal, highContrast] pair:

type HCPair<T> = T | [T, T];

It is used by tone, contrast, shadow intensity, and mix value:

tone: '-8'; // -8 in normal and HC
tone: ['-8', '-16']; // -8 normal, -16 HC
contrast: {
  apca: 'content';
} // preset with automatic HC enhancement
contrast: {
  apca: ['content', 'body'];
} // explicit normal/HC targets

For contrast, the pair may wrap the whole spec or live inside the selected metric. An explicit HC value disables automatic APCA enhancement or WCAG preset promotion for that color.


Manual contrast level

contrastLevel puts the normal end of the two-tier contrast model on a 0–100 slider. It is a config field, so it works globally, per theme, per token, and through extend():

glaze.configure({ contrastLevel: 60 });
const theme = glaze(280, 80, { contrastLevel: 60 });
glaze.color('#26fcb2', { contrastLevel: 60 });

The level moves the normal variants — light and dark — along that slider. It says nothing about high contrast: lightContrast / darkContrast stay the true high-contrast resolution at every level, and modes.highContrast alone decides whether they are emitted.

Value Meaning
'auto' Default. The normal variants take the authored normal entries as-is.
0 The same thing, reached through the slider — output-identical to 'auto'.
100 The normal variants are the high-contrast ones, so the tier is dropped.
anything between The normal variants are resolved at that level.

Levels 0 and 100 reproduce the classic light / dark and lightContrast / darkContrast output bit for bit.

What the level interpolates

Exactly the three things that make a high-contrast variant differ from its normal counterpart:

Mechanism At level L
Authored HCPairs — tone, contrast, shadow intensity, mix value the two ends are interpolated
The tone-window bypass each endpoint moves toward the full range: light 10→0, dark 15→0 / 95→100
Contrast escalation — AA → AAA, APCA +15 Lc the two numeric targets are interpolated (AA at level 50 solves for 5.75)

The interpolated values are then fed through the ordinary resolve, so a contrast floor at any level is solved, not approximated, and autoFlip decides once per level.

These feed the two normal passes only. The high-contrast passes bypass the level entirely, so the tier resolves exactly as it does in 'auto' mode.

Everything that does not vary by high contrast today — hue, saturation, darkDesaturation, opacity, pastel — is unaffected by the level.

Un-interpolable tone pairs switch at 50

Two ends of different kinds have no midpoint, because blending them would change which resolution rule applies partway up the ramp. These switch from the normal entry to the high-contrast one at level 50:

tone: [50, 'max']; // absolute vs. extreme
tone: [50, '+20']; // absolute vs. relative
tone: ['max', 'min']; // opposite extremes

Same-kind pairs ([30, 20], ['-6', '-12'], { wcag: [4.5, 7] }, [{ apca: 60 }, { apca: 90 }]) interpolate smoothly.

A contrast pair may not switch metric — [4.5, { apca: 75 }] throws. A WCAG ratio and an APCA Lc are different scales, so no target exists between them, and even in 'auto' mode the two variants would be incomparable. Pair values inside one metric instead: { wcag: [4.5, 7] } or { apca: [60, 90] }.

A color never swaps sides mid-slider

autoFlip decides per solve, and its tie-break — when both directions meet the floor, take the one nearer the authored tone — depends on the target. Along a ramp of targets that criterion shifts, so a naive implementation lets a color leap across its own base as the slider moves.

Under a manual level the side is instead decided once, from the nearer endpoint's target, and preferred at every level in that half of the ramp. Consequences:

  • A color whose two ends land on the same side of its base never changes side, at any level.
  • A color whose ends genuinely disagree — no single side can satisfy both its normal and its high-contrast floor — changes side exactly once, at level 50, the same place un-interpolable tone pairs switch.
  • Flipping is not disabled, only re-ordered, so a side that physically cannot reach the requested contrast still falls back to the opposite one. The floor is met at every level.

The underlying solver option is preferInitial on findToneForContrast, should you need the same behavior directly.

High-contrast output

The level and the high-contrast tier are independent, and they compose: the slider raises the baseline, and a prefers-contrast: more block still escalates on top of whatever baseline the user has chosen.

  • resolve() returns four variants at every level. lightContrast / darkContrast are the true high-contrast resolution — bit-identical to what 'auto' resolves for those slots, at any level.
  • modes.highContrast alone decides whether the tier is emitted, exactly as in 'auto' mode. A level does not turn it off.
  • css() always returns four strings and ignores modes, so its lightContrast / darkContrast strings now carry genuinely escalated values at a mid level, where they previously repeated the normal declarations. An existing @media (prefers-contrast: more) block starts doing real work.
  • A level on a single theme or token does not change which modes are emitted — modes is global-only, and a palette must not have one sibling collapse the shared token structure.
contrastLevel modes.highContrast: false (default) modes.highContrast: true
'auto' / 0 light, dark + lightContrast, darkContrast
199 light, dark at the level + the tier, unchanged by the level
100 (global) light, dark = the HC values same — the tier is dropped as a duplicate

That last row is the one exception. At level 100 the normal variants are the high-contrast ones, so a separate tier would be an exact duplicate: a global level of 100 emits a single light/dark set, and even an explicit modes: { highContrast: true } on the call is suppressed, because it has nothing different left to ask for. A level of 100 on a single theme or token still reports into the tier, with values equal to its own normal ones.

Driving it at runtime

configure() bumps the config version, which invalidates every theme and token cache, so a slider only needs to re-export:

slider.oninput = () => {
  glaze.configure({ contrastLevel: slider.valueAsNumber });
  apply(theme.css());
};

A manual resolve runs the same four passes as an 'auto' one. Only level 100 skips the two high-contrast passes, because the normal ones already produced those values.

Clearing it, exporting it, base links

  • configure() never clears a field by omission. Pass 'auto' explicitly to leave manual mode — globally, or on one theme of a palette.
  • .export() freezes only an authored level. A level set on the instance (or passed to .export()) is authored intent and is written to the snapshot; a level merely inherited from the global config is a live user preference and is left out, so a restored theme still follows the current slider. This matches how modes and states are treated.
  • Set the level globally, or on both sides of a base link. A per-token level on a dependent but not on its base anchors the two at different levels — the same caveat as a per-instance lightTone override. It is visible in the high-contrast tier too, since a dependent's high-contrast variant reads its base's high-contrast slots (and at level 100, the base's mirrored normal ones).

resolveContrastForLevel(spec, level, polarity?) is exported for advanced use; see Contrast solver.


Color definitions

ColorDef is a discriminated union:

type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;

RegularColorDef

Field Type Description
from GlazeColorValue Seed this color from a literal value (hex, rgb(), hsl(), okhsl(), okhst(), oklch(), or a value object). Supplies hue, tone, and an absolute saturation — the one way a theme color escapes the seed ceiling. Light/normal-contrast reproduces it exactly. See from.
tone HCPair<ToneValue> Number = absolute (0–100). '+N'/'-N' = a signed tone delta from the base (requires base). 'max'/'min' = forced to the scheme's tone extreme (no base). Optional HC pair [normal, hc]. Defaults to the tone of from.
saturation number Saturation factor applied to the seed saturation (0–1). Default: 1. With from, setting it overrides the color's absolute saturation and reverts to a factor of the seed.
hue number | RelativeValue Number = absolute (0–360). String ('+N'/'-N') = relative to the theme seed hue (never to a base color).
darkHue number | RelativeValue Dark-scheme hue. Number = absolute (0–360). String = relative to the theme dark seed hue. Falls back to hue. See Dark seed.
darkSaturation number Dark-scheme saturation factor (0–1) over the dark seed saturation. Falls back to saturation. When set, the global darkDesaturation reduction is not applied on top.
base string Name of another color in the same theme — makes this a dependent color.
contrast HCPair<ContrastSpec> Contrast floor against base. Requires base. See contrast.
mode 'auto' | 'fixed' | 'static' Adaptation mode. Default: 'auto'. See Adaptation modes.
autoFlip boolean Flip out-of-bounds results (relative tone overshoot / unmet contrast) to the opposite side instead of clamping. Default: the global autoFlip (true). See autoFlip.
opacity number Fixed alpha 0–1. Output includes alpha in the CSS value. Combining with contrast is not recommended (a console.warn is emitted).
pastel boolean Per-color override for the hue-independent "safe" chroma limit used in OKHSL↔sRGB conversions (luminance, contrast solving, output formatting). Falls through to the per-theme / per-token pastel override when omitted. Default: unset. See Per-color pastel.
role RoleInput Semantic role against base ('text' / 'surface' / 'border' or an alias). Fixes APCA contrast polarity. Resolved via: explicit role → name inference → opposite of the base's role → 'text'. See Roles.
inherit boolean Whether this color is inherited by child themes via extend(). Default: true. Set to false to make the color local to the current theme.

from (a literal color)

Most of Glaze answers the question "design me a palette". from answers the other one: "honor this color." White-label products, multi-tenant branding and imported design tokens all arrive with a value already chosen, and it is a contract rather than a starting point.

const theme = glaze(280, 80);

theme.colors({
  surface: { tone: 100, saturation: 0.12 },
  brand: { from: '#2f5bff', base: 'surface', contrast: 3 },
});

It accepts the same values as glaze.color() and supplies three things at once: hue, tone, and — uniquely among theme colors — an absolute saturation.

That last one is the point. Every other color's saturation is a 0–1 factor of the theme seed, so the seed is a ceiling: without from, the only way to place a color more saturated than its theme is to re-seed the whole theme, dragging every sibling along. A from color carries its own chroma and is unaffected by the seed:

// Identical output at every seed saturation — the color is the color.
for (const seed of [5, 50, 100]) {
  glaze(280, seed).colors({ brand: { from: '#2f5bff' } }); // → #2f5bff
}

What "exactly" covers. The light, normal-contrast variant reproduces the value (a local lightTone: false, the same default the value-shorthand form of glaze.color() applies). Dark and high contrast adapt as usual. That asymmetry is deliberate: those are the variants a reader reaches for when the normal one does not work for them, so readability outranks fidelity there — and a color pinned across all four would just be a worse mode: 'static'.

A contrast floor still applies in every scheme and is still a floor, not a target: a value that already clears it is emitted untouched, and one that misses moves only as far as the floor. Because the floor is solved per scheme, which scheme comes out exact depends on the color — a light brand cannot clear 3:1 on a white page but clears it easily on a dark one.

Sibling fields win over what the value supplied, so { from: '#2f5bff', hue: 300 } keeps the saturation and tone and rotates the hue. Note that a saturation written alongside from reverts to its usual meaning — a factor of the seed, not of the color.

A from color needs no base and no tone: it is placed absolutely, so it stands as a root on its own. Add base + contrast when it has to stay legible against something.

Tone values

tone (0–100) replaces authored OKHSL lightness with a contrast-shaped axis. Equal tone differences give equal WCAG contrast for neutrals; chromatic results can drift in measured luminance. See OKHST in Glaze. To port old lightness values, see migration.md.

Form Example Meaning
Number (absolute) tone: 45 Absolute tone 0–100.
String (tone delta) tone: '-52' Signed difference from the base color's resolved tone (requires base).
Extreme tone: 'max' / 'min' Force to the scheme's highest ('max' = 100) or lowest ('min' = 0) tone. With a base, the light-scheme shift is replayed in dark. No base needed.
HC pair tone: ['-7', '-20'] [normal, high-contrast]. A single value applies to both.

Absolute tone on a dependent color (base set) positions the color independently. In dark mode it is tone-mapped (inverted + windowed) on its own. The contrast solver acts as a safety net.

A tone delta applies a signed difference to the base color's resolved tone. It gives an exact contrast step for neutrals and a stable visual progression for chromatic colors. In dark mode with mode: 'auto', it is anchored to the base's per-scheme tone. If base + delta falls outside [0, 100], the result is clamped to the boundary, or — with autoFlip (default on) — mirrored to the other side of the base. If the mirrored target is also out of range, the original delta is kept and clamped on the authored side.

Extreme tone ('max' / 'min') forces the color to the scheme's tone extreme without a contrast hack or a magic number. 'max' resolves to author tone 100 and 'min' to 0. Without a base, both flow through scheme mapping like an absolute tone, so under mode: 'auto' they invert in dark ('max' is lightest in light, darkest in dark). Use mode: 'static' to pin the same extreme across schemes, or mode: 'fixed' to keep the same end without inverting. No base required.

Extreme tone with a base keeps the pair's contrast across schemes. The tone windows are asymmetric by default (lightTone: [10, 100], darkTone: [15, 95]), so re-mapping the extreme through the dark window would squeeze the base-to-extreme span and cost contrast. Instead Glaze measures the tone shift the light scheme applied between the base and the extreme, then replays it against the base's resolved dark tone — mirrored under mode: 'auto' (both ends invert), same-signed under 'fixed'. The result is clamped to [0, 100] only, so it may cross the darkTone window edge; that is intentional, since the author asked for the extreme. When the shift does not fit above or below the dark base, the color pins at tone 100 / 0. mode: 'static' is unaffected. High-contrast variants follow the same rule, but since their window is already the full range the replay reproduces the plain mapping unless the base itself sits asymmetrically across schemes.

theme.colors({
  bg: { tone: 60 },
  fg: { base: 'bg', tone: 'max' },
});
// light: bg 62.0, fg 100.0 — shift +38.0, contrast 3.18:1
// dark:  bg 43.9, fg 5.9   — shift -38.0, contrast 3.18:1

A dependent color with base but no tone inherits the base's tone (equivalent to a delta of 0).

autoFlip

autoFlip governs what happens when a result would fall outside its valid range:

  • Relative tone overshoot: when base ± delta exceeds [0, 100], autoFlip mirrors the delta to the other side of the base (e.g. '+30' becomes '-30') instead of clamping to the boundary. If the mirrored target is also outside [0, 100], the original delta is kept and clamped on the authored side.
  • contrast direction: when the requested tone direction can't meet the floor, autoFlip lets the solver try the opposite side (the same behavior as the global autoFlip).

autoFlip defaults to the global autoFlip (true). Set autoFlip: false on a color to clamp instead of mirror — useful when you want a relative offset to stay on the authored side of the base, or to keep an unmet contrast pinned to one direction's extreme.

contrast (floor)

type ContrastPreset = 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
type ContrastSpec =
  | number // bare WCAG ratio
  | ContrastPreset // named WCAG preset
  | { wcag: HCPair<number | ContrastPreset> }
  | { apca: HCPair<number | ApcaPreset> }; // APCA Lc target
Preset WCAG ratio
'AA-large' 3
'AA' 4.5
'AAA-large' 4.5
'AAA' 7

A bare number or preset means WCAG. Use { wcag } / { apca } to pick the metric explicitly. The [normal, highContrast] pair may live at the outer level ([4.5, 7], [{ wcag: 4.5 }, { wcag: 7 }]) or inside the metric ({ wcag: [4.5, 7] }, { apca: [45, 60] }).

contrast: 4.5; // WCAG 4.5
contrast: 'AAA'; // WCAG 7
contrast: {
  wcag: 6;
} // WCAG 6
contrast: {
  wcag: [4.5, 7];
} // WCAG 4.5 normal / 7 high-contrast (explicit)
contrast: {
  apca: 60;
} // APCA Lc 60 normal / 75 high-contrast (auto)
contrast: {
  apca: [45, 60];
} // APCA Lc 45 normal / 60 high-contrast (explicit)
contrast: {
  apca: 'content';
} // APCA preset -> Lc 60 normal / 75 high-contrast (auto)
contrast: {
  apca: ['content', 'body'];
} // Lc 60 normal / 75 high-contrast (explicit)

WCAG HC auto-promotion: a bare WCAG preset (no [normal, hc] pair at either the outer contrast or inner wcag level) is automatically promoted to its spec-defined "Enhanced" successor in high-contrast mode — AAAAA (4.5 → 7) and AA-largeAAA-large (3 → 4.5), per WCAG SC 1.4.3 → 1.4.6. AAA and AAA-large are already the top WCAG tier and are left unchanged; bare numeric targets have no successor tier and are also left unchanged. An explicit HC value via either pair overrides and skips the promotion.

APCA Enhanced Level (HC auto-boost): a bare APCA scalar (no [normal, hc] pair at either the outer contrast or inner apca level) is automatically boosted by +15 Lc in high-contrast mode, the APCA analog of WCAG's AAA-over-AA step. On by default; an explicit HC value via either pair overrides it and skips the boost. The enhanced target is clamped to 106 Lc. For large/bold text (where APCA caps contrast at Lc 90 to avoid glare), pass an explicit HC pair to hold that ceiling.

APCA preset keywords (Bronze Simple Mode conformance levels, role-independent): 'preferred' (Lc 90), 'body' (75), 'content' (60, ~AA), 'large' (45, ~3:1), 'non-text' (30), 'min' (15, point of invisibility).

The floor is applied independently per scheme. If the preferred tone already satisfies it, the tone is kept; otherwise the solver uses the tone-shaped scale for a closed-form WCAG seed and fast search until the target is met.

By default, the solver crosses to the opposite side of the base color when the requested tone direction cannot satisfy the floor. This is controlled per-color by autoFlip (which defaults to the global autoFlip). Set glaze.configure({ autoFlip: false }) — or autoFlip: false on a single color — to keep strict directionality: unmet colors pin to that direction's 0 or 100 tone extreme instead of falling back to the original requested value.

Full tone spectrum in HC mode: in high-contrast variants the lightTone and darkTone window constraints are bypassed entirely (the window is forced to [0, 100]). Colors can reach the full range, maximizing perceivable contrast.

Chromatic drift (verification): tone is contrast-uniform for grays. A chromatic swatch at a given tone shares its OKHSL lightness with the equivalent gray but drifts in real luminance, so a contrast-floored color may land slightly under its gray-tone expectation. Glaze measures the resolved result against the base and emits a deduped advisory console.warn when it drifts below the target. See Contrast verification.

Per-color hue override

const theme = glaze(280, 80);
theme.colors({
  surface: { tone: 97 },
  gradientEnd: { tone: 90, hue: '+20' }, // 280 + 20 = 300
  warning: { tone: 60, hue: 40 }, // absolute
});

Relative hue is always relative to the theme seed hue, not to a base color.

Dark seed (darkHue / darkSaturation)

Tone inverts automatically between light and dark, but hue and saturation do not — by default every scheme reuses the one seed, with dark getting a flat darkDesaturation haircut. When a palette needs a genuinely different chroma in dark (a cooler accent, a calmer surface tint), author a second seed:

const theme = glaze({
  hue: 280,
  saturation: 80,
  darkHue: 268, // dark schemes seed from 268 instead of 280
  darkSaturation: 65, // and from 65 instead of 80
});

Individual colors can override either channel for dark on their own:

theme.colors({
  surface: { tone: 97 },
  accent: { tone: 55, hue: '+20', darkHue: '+35' },
  warning: { tone: 60, saturation: 0.9, darkSaturation: 0.6 },
});

Both apply to the dark and darkContrast variants. The rules:

Rule Behavior
Units Seed-level darkSaturation is 0–100 (like saturation on the theme). Def-level darkSaturation is a 0–1 factor (like saturation on a color def).
Fallback darkHue falls back to hue, darkSaturation to saturation, and the def falls back to the seed. Omitting everything reproduces the previous behavior exactly.
Relative hue darkHue: '+N' anchors to the theme's dark seed hue. A plain hue: '+N' with no darkHue also re-anchors to the dark seed, so a whole palette rotates together.
Absolute hue An absolute hue (no darkHue) is used verbatim in dark — absolute means absolute in every scheme.
darkDesaturation Bypassed as soon as any explicit dark saturation is authored, at either level. The value you write is the value you get.
mode: 'static' Ignores both. A static color is pinned to one hue and saturation across every scheme.
Shadows and mixes Have no channels of their own — they derive hue and saturation from their bg / fg / target, so they inherit dark overrides automatically.

With splitHue, a dark seed makes the hue custom properties scheme-dependent; Glaze re-declares them in the dark block automatically.

Per-color pastel

pastel: true on a single color def overrides the per-theme / per-token pastel override for that color only. It toggles the hue-independent "safe" chroma limit used in every OKHSL↔sRGB conversion that touches this color: luminance calculations during contrast solving, gamut clamping during sRGB blend / mix edges, and output formatting. The effective flag is carried on the resolved variant (ResolvedColorVariant.pastel) so formatting matches the gamut mapping applied during resolution.

const theme = glaze(280, 80);
theme.colors({
  plain: { tone: 50, saturation: 1 },
  soft: { tone: 50, saturation: 1, pastel: true },
});
// theme.resolve().get('soft')!.light.pastel === true
// theme.css().light contains different rgb() triples for `--plain` and `--soft`

Omit the field to inherit the theme/token pastel override (default false) — useful for keeping the default behavior while opting a single accent into the pastel gamut.

The flag is part of the def object, so extend() copies it through to child themes alongside the rest of the def. Override it again on the child to flip a single color back:

const parent = glaze(280, 80);
parent.colors({ soft: { tone: 50, saturation: 1, pastel: true } });

const child = parent.extend({
  colors: { soft: { tone: 50, saturation: 1, pastel: false } },
});
// child.resolve().get('soft')!.light.pastel === false

Note: Per-color pastel is also supported on ShadowColorDef and MixColorDef (see the tables above). For shadows the math itself happens in OKHSL space, so the flag mainly controls the gamut-mapped output formatting and any luminance verification for that variant.

Standalone glaze.color() tokens accept the same pastel field on both the structured (GlazeColorInput) and value-shorthand (GlazeColorOverrides) forms, and it survives the export() / glaze.colorFrom() round-trip.

Roles

A color's role describes how it is used against its base and fixes APCA contrast polarity — which side is the foreground vs the background. APCA is asymmetric (|apca(a,b)| ≠ |apca(b,a)|), so the role picks the correct argument order; WCAG is symmetric and unaffected.

Role Polarity Use Aliases (name inference)
'text' fg Text / icons / foreground content text, fg, foreground, content, ink, label, stroke
'border' fg Non-text spot elements (borders, dividers, outlines) border, divider, outline, separator, hairline, rule
'surface' bg Backgrounds / fills surface, bg, background, fill, canvas, paper, layer

Resolution chain (per color):

  1. Explicit role (normalized from an alias) wins.
  2. Else, when inferRole is enabled (default), infer from the color name — the last recognized token wins (button-texttext, input-bgsurface, card-outlineborder).
  3. Else, the opposite of the base's role (a surface base ⇒ this is text).
  4. Else, 'text' (foreground) — i.e. the base is treated as the background.
const theme = glaze(280, 60);
theme.colors({
  surface: { tone: 90 },
  text: { base: 'surface', contrast: { apca: 'content' } }, // inferred text
  border: { base: 'surface', tone: '-10' }, // inferred border
});
// role fixes APCA polarity; set `pastel: true` explicitly if a border
// needs the hue-independent safe chroma limit.

Disable name inference with glaze.configure({ inferRole: false }) (the base-opposite and foreground-default fallbacks still apply).

ShadowColorDef

Field Type Description
type 'shadow' Discriminator.
bg string Background color name — must reference a non-shadow color in the same theme.
fg string Optional foreground color name for tinting and intensity modulation. Must reference a non-shadow color. Omit for an achromatic shadow at full user-specified intensity.
intensity HCPair<number> Shadow intensity, 0–100. Supports HC pairs.
tuning ShadowTuning Per-color tuning overrides. Merged field-by-field with the global shadowTuning.
pastel boolean Per-color pastel override. See Per-color pastel.
inherit boolean Inheritance flag, default true.

See Shadows below for the algorithm and tuning details.

MixColorDef

Field Type Description
type 'mix' Discriminator.
base string "From" color name.
target string "To" color name.
value HCPair<number> Mix ratio 0–100 (0 = pure base, 100 = pure target). In 'transparent' blend, this becomes the target's opacity. Supports HC pairs.
blend 'opaque' | 'transparent' Default 'opaque'.
space 'okhsl' | 'srgb' Interpolation space for opaque blending. Default 'okhsl'. Ignored for 'transparent' (always composites in linear sRGB).
contrast HCPair<ContrastSpec> Optional contrast floor against base (WCAG or APCA — see contrast). The solver adjusts the mix ratio (opaque) or opacity (transparent).
pastel boolean Per-color pastel override. See Per-color pastel.
role RoleInput Semantic role of the mixed result against base. Same semantics as RegularColorDef.role (see Roles).
inherit boolean Inheritance flag, default true.

See Mix colors below.


Standalone color tokens

glaze.color() creates a single color token without a full theme.

// arg1: the color (four shapes — see below)
// arg2: optional config override (GlazeConfigOverride — see below)
glaze.color(color: GlazeFromInput | GlazeColorInput | GlazeColorValue, config?: GlazeConfigOverride): GlazeColorToken;

Input forms

glaze.color() accepts four input shapes, discriminated by structure:

Shape Example Notes
Bare string '#26fcb2' Hex or CSS color function (rgb(), hsl(), okhsl(), okhst(), oklch()).
Value object { h: 152, s: 0.95, l: 0.74 } OKHSL, OKHST ({ h, s, t }), { r, g, b } (sRGB 0–255), or { l, c, h } (OKLCh).
{ from, ...overrides } { from: '#1a1a2e', base: bg, contrast: 'AA' } Value + color overrides in one object.
Structured { hue: 152, saturation: 95, tone: 74 } Full theme-style token (hue/saturation in 0–100, tone in 0–100).

GlazeColorValue (bare string or value-object forms) accepts:

Form Example Notes
Hex '#26fcb2', '#26fcb2ff', '#abc' 3, 6, or 8 digits. Alpha is dropped with a console.warn — use opacity instead.
rgb() 'rgb(38 252 178)', 'rgb(38 252 178 / 0.8)' Modern space syntax. Alpha dropped with warning.
hsl() 'hsl(152 97% 57%)' Modern space syntax. Alpha dropped with warning.
okhsl() 'okhsl(152 95% 74%)' Glaze's own emit format. Alpha dropped with warning.
okhst() 'okhst(152 95% 70%)' OKHST tone input (third value is tone 0–100). Not native CSS; it can also be serialized by Tasty exports. Alpha dropped with warning.
oklch() 'oklch(0.85 0.18 152)' Glaze's own emit format. Alpha dropped with warning.
OkhslColor object { h: 152, s: 0.95, l: 0.74 } OKHSL shape (h: 0–360, s/l: 0–1). Passing 0–100 for s/l throws with a hint to use the structured form.
OkhstColor object { h: 152, s: 0.95, t: 0.70 } Direct OKHST input shape (h: 0–360, s/t: 0–1). The t key disambiguates it from { h, s, l }.
RgbColor object { r: 38, g: 252, b: 178 } sRGB 0–255. RGB tuple [r, g, b] is not supported — use this object form.
OklchColor object { l: 0.85, c: 0.18, h: 152 } OKLCh (L/C: 0–1, H: degrees), same semantics as oklch() strings.

GlazeColorInput (structured form) is { hue, saturation, tone, ... }:

Field Type Description
hue number 0–360.
saturation number 0–100.
tone HCPair<number | ExtremeValue> 0–100 (contrast-shaped) or 'max'/'min', optional HC pair.
saturationFactor number Multiplier on the seed (0–1). Default: 1.
darkHue number | RelativeValue Dark-scheme hue. Absolute (0–360) or '+N'/'-N' relative to the dark seed hue (which defaults to hue). Falls back to hue.
darkSaturation number Dark-scheme seed saturation (0–100). Falls back to saturation.
darkSaturationFactor number Dark-scheme multiplier on the dark seed (0–1). Falls back to saturationFactor.
mode AdaptationMode Default: 'auto'.
autoFlip boolean Flip out-of-bounds results instead of clamping. Default: global autoFlip.
opacity number Fixed alpha 0–1.
base GlazeColorToken | GlazeColorValue Optional dependency. See Pairing colors.
contrast HCPair<ContrastSpec> Contrast floor against base (WCAG or APCA). Without base, anchored to the literal seed.
pastel boolean Per-color pastel override. Falls through to the per-theme / per-token pastel override when omitted. See Per-color pastel.
role RoleInput Semantic role against base / the seed (see Roles). Fixes APCA polarity.
name string Debug label for warnings; doesn't change output keys. Reserved names ('value', 'seed', 'externalBase') are rejected.

GlazeFromInput (from form) is { from: GlazeColorValue, ...colorOverrides }:

Field Notes
from Required. The source color value — same forms as GlazeColorValue.
hue Number (absolute 0–360) or '+N'/'-N' (relative to seed, never to base).
saturation Override seed saturation (0–100).
tone Number (absolute 0–100), '+N'/'-N', or 'max'/'min'. Without base, relative anchors to the seed; with base, anchors to base's tone per scheme.
saturationFactor Multiplier on the seed (0–1).
darkHue Dark-scheme hue: absolute (0–360) or '+N'/'-N' relative to the dark seed hue. Falls back to hue.
darkSaturation Dark-scheme seed saturation (0–100). Falls back to saturation.
darkSaturationFactor Dark-scheme multiplier on the dark seed (0–1). Falls back to saturationFactor.
mode 'auto' (default) / 'fixed' / 'static'.
autoFlip Flip out-of-bounds results instead of clamping. Default: global autoFlip.
contrast Contrast floor (WCAG or APCA). Without base, anchored to the literal seed; with base, solved per scheme.
base GlazeColorToken or raw GlazeColorValue. See Pairing colors.
opacity Fixed alpha 0–1. Combining with contrast is not recommended — console.warn is emitted.
pastel Per-color pastel override. Falls through to the per-theme / per-token pastel override when omitted. See Per-color pastel.
role Semantic role against base / the seed (see Roles). Fixes APCA polarity.
name Debug label only — surfaces in warnings/errors. Does not change output keys.

Named CSS colors ('red', 'blueviolet') are not supported.

Defaults

Every input form defaults to mode: 'auto' so the resolved token adapts between light and dark like an ordinary theme color. Tokens store a sparse local config override; omitted fields fall through to the live global at resolve time (same as themes). Authoring .export(override?) freezes getConfig() ∪ local ∪ override at call time.

  • Value-shorthand (bare strings, value objects, and { from, ...overrides }):
    • Light variant preserves the input tone exactly (lightTone: false as a local default).
    • Other omitted fields track the live global config.
  • Structured input ({ hue, saturation, tone, ... }):
    • Omitted tone windows and other fields track the live global config (same as a theme color).
  • pastel is instance-only — set via the config override or per-color pastel, not glaze.configure().
// Bare string — adapts automatically
glaze.color('#26fcb2');

// Value-object — same behavior
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 });

// From form — value + color overrides
glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' });

// Structured form — explicit hue/saturation/tone (0–100)
glaze.color({ hue: 152, saturation: 95, tone: 74 });

Token methods

A GlazeColorToken exposes:

Method Description
token.resolve() Resolve as a ResolvedColor (light/dark/lightContrast/darkContrast variants).
token.token(options?) Flat token map (no color-name key). Options: format, modes, states.
token.tasty(options?) Tasty state map (no color-name key). Same options as token.token.
token.json(options?) JSON map (no color-name key). Options: format, modes.
token.css({ name, format?, suffix? }) CSS custom property declarations grouped by scheme variant. name is required and becomes the variable identifier ('brand'--brand-color). Defaults: format: 'rgb', suffix: '-color' (matches theme.css).
token.dtcg(options?) DTCG color tokens, one per scheme variant (no color-name key). Each entry is a full { $type: 'color', $value } token. Options: colorSpace ('srgb' | 'oklch'), modes.
token.dtcgResolver({ name, ... }) A single DTCG Resolver-Module document for this color, keyed by name across all scheme variants. name is required. Same options as theme.dtcgResolver() plus name.
token.tailwind({ name, ... }) Tailwind v4 @theme block + dark / high-contrast overrides for this color. name is required (forms --color-<name>). Same options as theme.tailwind() plus name.
token.export(override?) JSON-safe snapshot — freezes effective config at call time; pass to glaze.colorFrom(...) to rehydrate. Optional override merges over the instance local (and nested base exports).

Per-instance config override

The optional config argument (GlazeConfigOverride) overrides resolve-relevant fields for a token or theme. A tone window can be [lo, hi], { lo, hi, eps }, or false (full range). Both themes and standalone tokens keep a sparse local override — omitted fields (except instance-only pastel) fall through to the live global at resolve time.

GlazeConfigOverride:

Field Default (from global / fixed) Description
lightTone [10, 100] Light tone window: [lo, hi], { lo, hi, eps }, or false (disable clamping).
darkTone [15, 95] Dark tone window: [lo, hi], { lo, hi, eps }, or false (disable clamping).
darkDesaturation 0.1 Saturation reduction in dark scheme (0–1).
autoFlip true Default for each color's autoFlip: when solving contrast (or applying a relative tone that overshoots), allow crossing to the opposite side instead of clamping.
pastel false (instance-only) Theme/token-level pastel default for colors that omit per-color pastel. Not available on glaze.configure().
inferRole true Infer color role from the name when unset.
shadowTuning undefined Default shadow tuning (meaningful for themes; harmless on color tokens).

Config overrides apply to both glaze.color() tokens and glaze() themes:

// Standalone color — preserve raw tone in both schemes
glaze.color('#26fcb2', { darkTone: false });

// Restore the #000 → white dark flip (full dark range)
glaze.color('#000000', {
  lightTone: false,
  darkTone: [15, 100],
});

// Structured form with config override
glaze.color({ hue: 152, saturation: 95, tone: 74 }, { darkTone: false, pastel: true });

// Theme with config override
const rawTheme = glaze(280, 80, { lightTone: false, pastel: true });

See Theme config override.

Theme config override

When a theme is created with a GlazeConfigOverride, the override is merged over the live global config at resolve time. This means:

  • Fields you overrode are fixed — glaze.configure() can't change them for this theme.
  • Fields you didn't override still react to later glaze.configure() calls.
const t = glaze(280, 80, { lightTone: [0, 50] });
t.colors({ text: { tone: 50, saturation: 1 } });
// text.light lands inside the [0, 50] window — always, regardless of
// global lightTone changes.
// text.dark.s reacts to glaze.configure({ darkDesaturation }) since it's not overridden.

extend inherits the parent's override and shallow-merges the child's:

const child = t.extend({ config: { darkTone: false } });
// child: lightTone { lo: 0, hi: 50 } (inherited) + darkTone: false (added)

theme.export(override?) freezes getConfig() ∪ instance local ∪ override at call time. Restoring via glaze.themeFrom(data) (or the compat alias glaze.from) pins that freeze as the restored theme's local override — matching standalone color-token behavior.

glaze.colorFrom(data)

Inverse of token.export(). The exported snapshot includes the original input, all overrides (with any base token recursively serialized), and the effective config freeze from export time — so later glaze.configure() calls don't change rehydrated tokens.

const text = glaze.color({ from: '#1a1a1a', contrast: 'AA' });
const data = text.export();
const restored = glaze.colorFrom(data);
// restored.resolve() === text.resolve() byte-for-byte

Both value-form and structured-form tokens round-trip.

Pairing colors

Set base to anchor a standalone color to another standalone color or raw value. The contrast solver and relative tone offsets switch their anchor from the literal seed to the base's resolved variant per scheme — so the same text color automatically lands at AA against its background in light, dark, and high-contrast modes.

const bg = glaze.color('#1a1a2e');

// Text guaranteed AA against `bg` in every scheme.
const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });

// Border 8 tone units lighter than `bg` in each scheme.
const border = glaze.color({
  from: '#000000',
  base: bg,
  tone: '+8',
  mode: 'fixed',
});

// Raw-value base — Glaze auto-wraps it via `glaze.color(value)`.
const text2 = glaze.color({ from: '#ffffff', base: '#1a1a2e', contrast: 'AA' });

Behavior with base:

  • contrast is solved per scheme against base's resolved variant (light / dark / lightContrast / darkContrast).
  • Relative tone: '+N' / '-N' is anchored to base's tone per scheme (matches theme behavior).
  • Relative hue: '+N' / '-N' still anchors to the seed (the value passed to glaze.color()), not the base.
  • mode works as a per-pair knob.
  • The base token's .resolve() is called lazily on the first resolve of the dependent and the result is captured by reference; later mutations to the base don't apply.
  • Structured bases are resolved at full range for linking math: when a value/from color links to a base created via the structured form, the contrast/tone anchor uses the raw input tone (not the windowed output). This ensures the anchor matches what you intended, not what the light window remapped it to. The base's own .resolve() output is unaffected.
  • When the contrast target is physically unreachable, glaze emits a single console.warn per (name, scheme, target) triple and returns the closest passing variant. Use the name override to make the warning identifiable.

Chains compose:

const bg = glaze.color('#000000');
const surface = glaze.color({ from: '#222222', base: bg, contrast: 'AAA' });
const text = glaze.color({ from: '#ffffff', base: surface, contrast: 'AA' });

name is a debug label

The name override appears in console.warn / Error messages but does not change output keys (.token(), .tasty(), .json(), .css() still use '', light, etc.). The CSS variable name comes from css({ name }), not from the override.


Shadows

Defining shadow colors in a theme

theme.colors({
  surface: { tone: 95 },
  text: { base: 'surface', tone: '-52', contrast: 'AAA' },

  'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
  'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
  'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
});

Shadow colors are included in all output methods (tokens(), tasty(), css(), json()) alongside regular colors and emit an alpha component:

'oklch(0.15 0.009 282 / 0.1)'
'rgb(34 28 42 / 0.1)'

How shadows work

  1. Contrast weight — when fg is provided, shadow strength scales with |l_bg − l_fg|. Dark text on a light background produces a strong shadow; near-background-lightness elements produce barely visible shadows.
  2. Pigment color — hue blended between fg and bg, low saturation, dark lightness.
  3. Alpha — computed via a tanh curve that saturates smoothly toward alphaMax (default 1.0), ensuring well-separated shadow levels even on dark backgrounds.

Omit fg for an achromatic shadow at full user-specified intensity:

theme.colors({
  'drop-shadow': { type: 'shadow', bg: 'surface', intensity: 12 },
});

intensity supports [normal, highContrast] pairs:

'shadow-card': { type: 'shadow', bg: 'surface', fg: 'text', intensity: [10, 20] },

ShadowTuning

Fine-tune behavior per-color or globally via glaze.configure({ shadowTuning }). Per-color tuning is merged field-by-field with the global one.

Parameter Default Description
saturationFactor 0.18 Fraction of fg saturation kept in pigment.
maxSaturation 0.25 Upper clamp on pigment saturation.
lightnessFactor 0.25 Multiplier for bg lightness → pigment lightness.
lightnessBounds [0.05, 0.20] Clamp range for pigment lightness.
minGapTarget 0.05 Target minimum gap between pigment and bg lightness.
alphaMax 1.0 Asymptotic maximum alpha.
bgHueBlend 0.2 Blend weight pulling pigment hue toward bg hue. 0 = pure fg hue, 1 = pure bg hue.
theme.colors({
  'shadow-soft': {
    type: 'shadow',
    bg: 'surface',
    intensity: 10,
    tuning: { alphaMax: 0.3, saturationFactor: 0.1 },
  },
});

glaze.configure({
  shadowTuning: { alphaMax: 0.5, bgHueBlend: 0.3 },
});

Standalone shadow computation

glaze.shadow(input) computes a shadow outside of a theme. bg and fg accept any GlazeColorValue:

const v = glaze.shadow({
  bg: '#f0eef5',
  fg: '#1a1a2e',
  intensity: 10,
});
// → { h: 280, s: 0.14, l: 0.2, alpha: 0.1 }

const css = glaze.format(v, 'oklch');
// → 'oklch(0.15 0.014 280 / 0.1)'

GlazeShadowInput:

Field Type Description
bg GlazeColorValue Background. Any GlazeColorValue form. Alpha components dropped with warning.
fg GlazeColorValue Optional foreground. Same forms as bg.
intensity number 0–100.
tuning ShadowTuning Optional.

Fixed opacity (regular colors)

For a simple fixed-alpha color (no shadow algorithm), use opacity on a regular color:

theme.colors({
  overlay: { tone: 0, opacity: 0.5 },
});
// → 'oklch(0 0 0 / 0.5)'

Mix colors

Opaque mix

Produces a solid color by interpolating between base and target:

theme.colors({
  surface: { tone: 95 },
  accent: { tone: 30 },
  tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
});
  • value: 0 = pure base, value: 100 = pure target.
  • Result has alpha = 1.
  • Adapts to light/dark/HC schemes automatically via the resolved base and target.

Transparent mix

Produces the target color with controlled opacity — useful for hover overlays:

theme.colors({
  surface: { tone: 95 },
  black: { tone: 0, saturation: 0 },
  hover: {
    type: 'mix',
    base: 'surface',
    target: 'black',
    value: 8,
    blend: 'transparent',
  },
});
// hover → black with alpha = 0.08

The output color has h, s, l from the target and alpha = value / 100.

Blend space (opaque only)

space Behavior Best for
'okhsl' (default) Perceptually uniform OKHSL interpolation. Design token derivation.
'srgb' Linear sRGB channel interpolation. Matching browser compositing of CSS color-mix / overlay.

Transparent blending always composites in linear sRGB (matches browser alpha compositing).

Contrast solving on mixes

Mix colors support the same contrast prop as regular colors. The solver adjusts the mix ratio (opaque) or opacity (transparent) to meet the WCAG target:

'tint': {
  type: 'mix', base: 'surface', target: 'accent',
  value: 10, contrast: 'AA',
},
'overlay': {
  type: 'mix', base: 'surface', target: 'accent',
  value: 5, blend: 'transparent', contrast: 3,
},

Both value and contrast support [normal, highContrast] pairs.

Achromatic colors

When mixing with achromatic colors (saturation near zero, e.g. white or black) in okhsl space, the hue comes from whichever color has saturation. Matches CSS color-mix() "missing component" behavior. For purely achromatic mixes prefer space: 'srgb' where hue is irrelevant.

Mix chaining

Mix colors can reference other mix colors:

theme.colors({
  white: { tone: 100, saturation: 0 },
  black: { tone: 0, saturation: 0 },
  gray: {
    type: 'mix',
    base: 'white',
    target: 'black',
    value: 50,
    space: 'srgb',
  },
  lightGray: {
    type: 'mix',
    base: 'white',
    target: 'gray',
    value: 50,
    space: 'srgb',
  },
});

Mix colors cannot reference shadow colors (same restriction as regular dependent colors).


Palette

glaze.palette(themes, options?) composes multiple themes into a single token namespace.

const palette = glaze.palette({ primary, danger, success, warning });
const palette = glaze.palette(
  { primary, danger, success },
  { primary: 'primary' },
);

GlazePaletteOptions:

Option Description
primary Name of the primary theme. The primary's tokens are duplicated without prefix in all exports, providing convenient short aliases alongside the prefixed versions. Throws if the name doesn't match any theme.

A GlazePalette exposes:

Method Description
palette.list() Theme names in insertion order.
palette.primary Primary theme name, if set.
palette.theme(name) Get a live theme instance by name.
palette.themes() Shallow copy of the theme map (same instances the palette holds).
palette.export(override?) Authoring snapshot — restorable via glaze.paletteFrom(). Optional override forwarded to themes.
palette.tokens(options?) Flat token map grouped by scheme variant.
palette.tasty(options?) Tasty style-to-state bindings.
palette.json(options?) Per-theme resolved color JSON (not restorable as authoring config).
palette.css(options?) CSS custom property declaration strings.
palette.dtcg(options?) Per-scheme W3C DTCG token trees.
palette.dtcgResolver(options?) One DTCG Resolver-Module document for every scheme.
palette.tailwind(options?) One Tailwind CSS v4 theme with scheme overrides.

palette.export(override?) / glaze.paletteFrom()

const snapshot = palette.export();
// → {
//     kind: 'palette',
//     version: 1,
//     primary: 'brand',
//     themes: { brand: { kind: 'theme', ... }, danger: { ... } },
//   }

const restored = glaze.paletteFrom(JSON.parse(JSON.stringify(snapshot)));
const brand = restored.theme('brand')!;

Optional override is forwarded to each nested theme.export(override). Config snapshot vs resolved output: use export() / paletteFrom() to persist and restore the authoring graph (themes, color defs, relations). Use json() / tokens() / css() / … to emit resolved color strings for apps and design tools.

GlazePaletteExportOptions

Shared by tokens, tasty, and css:

Option Default Description
prefix true (= "<themeName>-") false disables prefixing. Or pass a custom map: { primary: 'brand-', danger: 'error-' }.
primary inherits from palette creation string to override, false to disable for this call.

Each export method also accepts its own format/options shape:

Method Additional options
palette.tokens(options?) format, modes
palette.tasty(options?) format, modes, states
palette.css(options?) format, suffix
palette.dtcg(options?) colorSpace, modes
palette.dtcgResolver(options?) colorSpace, modes, resolver names
palette.tailwind(options?) format, modes, selectors, namespace

palette.css() does not accept modes; it always returns all four CSS strings (light, dark, lightContrast, darkContrast).

Prefix behavior

By default all palette tokens are prefixed:

palette.tokens();
// → {
//   light: { 'primary-surface': 'oklch(...)', 'danger-surface': 'oklch(...)' },
//   dark:  { 'primary-surface': 'oklch(...)', 'danger-surface': 'oklch(...)' },
// }

Custom map (any theme not listed falls back to "<themeName>-"):

palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });

Disable prefixing:

palette.tokens({ prefix: false });

Collision detection

When two themes produce the same output key (via prefix: false, custom prefix maps, or primary unprefixed aliases), the first-written value wins and a console.warn is emitted:

glaze: token "surface" from theme "b" collides with theme "a" — skipping.

Primary theme aliases

The primary theme's tokens are duplicated without prefix:

const palette = glaze.palette(
  { primary, danger, success },
  { primary: 'primary' },
);
palette.tokens();
// → {
//   light: {
//     'primary-surface': 'oklch(...)',
//     'danger-surface':  'oklch(...)',
//     'success-surface': 'oklch(...)',
//     'surface':         'oklch(...)',  // unprefixed alias
//   },
// }

Override per-export:

palette.tokens({ primary: 'danger' });
palette.tokens({ primary: false });

The primary alias works alongside any prefix mode — when using a custom map, primary tokens are still duplicated without prefix:

palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
// → 'p-surface' + 'surface' (alias) + 'd-surface'

palette.json()

JSON export groups by theme name (no prefix needed):

palette.json();
// → {
//   primary: { surface: { light: 'oklch(...)', dark: 'oklch(...)' } },
//   danger:  { surface: { light: 'oklch(...)', dark: 'oklch(...)' } },
// }

palette.css()

const css = palette.css();
const stylesheet = `
:root { ${css.light} }
@media (prefers-color-scheme: dark) {
  :root { ${css.dark} }
}
`;

palette.css() accepts the same GlazeCssOptions as theme.css() plus GlazePaletteExportOptions. It does not accept modes; all four result fields are always returned.

palette.dtcg()

DTCG export for a palette. Prefix defaults to true and the palette-level primary is honored (the primary theme's tokens are duplicated without prefix as aliases).

palette.dtcg();
// → {
//   light: {
//     'primary-surface': { $type: 'color', $value: { ... } },
//     'surface':         { $type: 'color', $value: { ... } },  // unprefixed alias
//     'danger-surface':  { $type: 'color', $value: { ... } },
//   },
//   dark: { ... },
// }

Accepts GlazeDtcgOptions plus GlazePaletteExportOptions.

palette.dtcgResolver()

Resolver-Module export for a palette. Same as theme.dtcgResolver() but merges every theme (with prefix / primary aliasing) into the single sets.base source and each scheme context. Prefix defaults to true; the palette-level primary is honored.

palette.dtcgResolver();
// → {
//   version: '2025.10',
//   sets: { base: { sources: [ { 'primary-surface': {…}, 'surface': {…}, 'danger-surface': {…} } ] } },
//   modifiers: { scheme: { default: 'light', contexts: { light: [], dark: [ {…} ] } } },
//   resolutionOrder: [ { $ref: '#/sets/base' }, { $ref: '#/modifiers/scheme' } ],
// }

Accepts GlazeDtcgResolverOptions plus GlazePaletteExportOptions.

palette.tailwind()

Tailwind export for a palette. All themes are merged into a single @theme block (plus dark / high-contrast overrides), so each color is reachable as a Tailwind utility. Prefix defaults to true.

const css = palette.tailwind();
// @theme {
//   --color-primary-surface: oklch(...);
//   --color-surface: oklch(...);          /* unprefixed alias */
//   --color-danger-surface: oklch(...);
// }
// .dark { ... }

Accepts GlazeTailwindOptions plus GlazePaletteExportOptions. The palette prefix option (theme prefixing) is separate from GlazeTailwindOptions.namespace (the --color-* CSS namespace).


Output formats

Control the color format with the format option on any export method:

Format Output (alpha = 1) Output (alpha < 1) Notes
'oklch' (default for CSS-string exports) oklch(L C H) oklch(L C H / A) OKLab-based LCH. Native CSS. Required for splitHue.
'rgb' rgb(R G B) rgb(R G B / A) Rounded integers, modern space syntax.
'hsl' hsl(H S% L%) hsl(H S% L% / A) Modern space syntax.
'okhsl' okhsl(H S% L%) okhsl(H S% L% / A) Glaze's native format, not a CSS function. Tasty-only (tasty(), token(), .tasty()).
'okhst' okhst(H S% T%) okhst(H S% T% / A) OKHST tone axis. Tasty-only — same restriction as okhsl.
theme.tokens(); // 'oklch(0.965 0.0123 280)'  (default)
theme.tokens({ format: 'rgb' }); // 'rgb(244 240 250)'
theme.tasty(); // 'oklch(0.965 0.0123 280)'   (default)
theme.tasty({ format: 'okhst' }); // 'okhst(280 60% 97%)'

All numeric output strips trailing zeros for cleaner CSS (e.g. 95 not 95.0).

The format option works on CSS-string exports: theme.tokens(), theme.tasty(), theme.json(), theme.css(), theme.tailwind(), the same on palette, and on token.token() / .tasty() / .json() / .css() / .tailwind(). okhsl and okhst throw on non-Tasty exports (tokens, json, css, tailwind) — they are not native CSS color spaces.

Hue channel splitting (splitHue)

On theme.css(), theme.tasty(), palette.css(), palette.tasty(), and standalone color.css() with format: 'oklch', set splitHue: true to emit hue as its own custom property so consumers can re-skin at runtime:

/* theme.css({ format: 'oklch', splitHue: true, name: 'brand' }) */
--brand-hue: 240;
--accent-hue: calc(var(--brand-hue) + 20);
--surface-color: oklch(0.52 0.06 var(--brand-hue));
--accent-color: oklch(0.62 0.03 var(--accent-hue));

Requirements: every exported color must be pastel (pastel: true on the theme/token override or per-color). Pastel mode bounds chroma by the hue-independent safe chroma at each lightness, so emitted C stays in sRGB for any rotated hue. Non-pastel palettes throw rather than emit values that would clip under rotation.

Dark hues: hue vars are scheme-independent unless the theme or a color authors a dark hue. When one does, the whole hue set is re-declared in the dark block (and in the Tasty dark state, gated by modes.dark):

/* glaze({ hue: 240, saturation: 18, darkHue: 200 }) */
:root {
  --brand-hue: 240;
  --accent-hue: calc(var(--brand-hue) + 20);
}
/* dark */
--brand-hue: 200;
--accent-hue: calc(var(--brand-hue) + 20);

--accent-hue repeats verbatim on purpose: a custom property substitutes var() at computed-value time on the element that declares it, so it has to be re-declared to pick up the new --brand-hue. Colors with mode: 'static' are pinned to an absolute hue instead, so they don't drift with the dark seed.

Limitations: oklch only (native CSS var() in the hue slot). Shadow and mix colors stay inline (blended hue). Standalone .token() / .tasty() do not support splitHue (return shape cannot carry the $name-hue declaration).

theme.dtcg() / theme.dtcgResolver() / palette.dtcg() / palette.dtcgResolver() ignore format — DTCG emits structured $value objects, not CSS strings. Use the colorSpace option ('srgb' or 'oklch') to pick the color representation instead.


Adaptation modes

mode controls how a color adapts across schemes:

Mode Behavior
'auto' (default) Full adaptation. Dark uses dark tone inversion (100 − t) and then remaps into the dark tone window. High-contrast uses the full range.
'fixed' Color stays recognizable. Tone is mapped (not inverted) into the dark window. Use for brand buttons, CTAs, status banners.
'static' No adaptation. Same tone in every scheme.

How relative tone adapts

auto — the offset is anchored to the base's per-scheme tone:

Light: surface tone=97, text tone='-52' → tone 45 (dark text on light bg)
Dark:  surface inverts to a low tone; the '-52' offset re-anchors to the
       base's light tone and maps into the dark window (light text on dark bg)

fixed — tone is mapped (not inverted), relative sign preserved:

Light: accent-fill tone=52, accent-text tone='+20' → lighter than the fill
Dark:  accent-fill maps into the dark window, sign preserved

Offsets that would push past [0, 100] clamp to the boundary, or — with autoFlip (default on) — mirror to the other side of the base. If the mirror also overshoots, the original side is kept and clamped. Set autoFlip: false to keep the authored side and clamp instead.

static — no adaptation, same tone in every scheme.


Light / dark scheme mapping

The mapping is a single tone pipeline; there is no Möbius curve. See Scheme adaptation for the product-level model and the canonical OKHST specification for the transfer math.

Light scheme

An authored tone (0–100) is remapped into the lightTone tone window. The window's lo/hi are OKHSL-lightness boundaries (0–100); authored tone is positioned within the corresponding tone interval and converted to final OKHSL lightness. static mode and HC variants use the full range.

window      = lightTone               // default [10, 100]
finalTone   = remap(authorTone, window)
finalL      = fromTone(finalTone)     // OKHSL lightness

Dark scheme

auto — invert the tone, then remap into the dark window:

window    = darkTone                  // default [15, 95]
inverted  = 100 - authorTone
finalTone = remap(inverted, window)

The inversion preserves authored tone spacing without a fitted curve. This is exactly contrast-even for neutrals and approximate for chromatic colors. The ordinary light/dark asymmetry lives in the two windows' (lo, hi, eps) values (eps defaults to the reference 0.05).

fixed — remap into the dark window without inversion:

finalTone = remap(authorTone, darkTone)

In high-contrast variants both windows are bypassed (forced to the full [0, 100] range): auto still inverts, fixed/static do not.

Dark scheme — saturation

darkDesaturation reduces saturation for all colors in dark scheme:

S_dark = S_light * (1 - darkDesaturation); // default: 0.1

static mode skips desaturation.


Configuration

glaze.configure({
  lightTone: [10, 100], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
  darkTone: [15, 95], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
  darkDesaturation: 0.1,
  states: {
    dark: '@media(prefers-color-scheme: dark)',
    highContrast: '@media(prefers-contrast: more)',
  },
  modes: {
    dark: true,
    highContrast: false,
  },
  shadowTuning: {
    alphaMax: 0.6,
    bgHueBlend: 0.2,
  },
  contrastLevel: 'auto', // or 0–100 for a manual contrast slider
});

A ToneWindow is [lo, hi] (OKHSL-lightness boundaries, reference eps — the common form), { lo, hi, eps } (advanced: explicit per-scheme render eps), or false for the full range [0, 100] at the reference eps. false removes the boundaries, not the tone transfer.

GlazeConfig:

Field Default Description
lightTone [10, 100] Light scheme tone window: [lo, hi], { lo, hi, eps }, or false to disable clamping. Bypassed in HC; widened continuously by contrastLevel.
darkTone [15, 95] Dark scheme tone window: [lo, hi], { lo, hi, eps }, or false to disable clamping. Bypassed in HC; widened continuously by contrastLevel.
darkDesaturation 0.1 Saturation reduction in dark scheme (0–1).
states.dark '@media(prefers-color-scheme: dark)' State alias for dark mode tokens (Tasty export). Defaults to a media query so tokens react to the OS preference without registering custom states.
states.highContrast '@media(prefers-contrast: more)' State alias for HC tokens (Tasty export).
modes.dark true Include dark variants in exports.
modes.highContrast false Include HC variants. Independent of contrastLevel — the level positions the normal variants while these stay the true HC resolution. A global level of 100 is the exception: the tier would duplicate the normal set, so it is dropped.
shadowTuning undefined Default tuning for all shadow colors. Per-color tuning merges field-by-field.
autoFlip true Default for each color's autoFlip. When solving contrast (or applying a relative tone that overshoots [0, 100]), allow crossing to the opposite side instead of clamping. With false, only the requested direction is considered; unmet contrasts pin the tone to that direction's extreme (and emit a warning) and overshooting offsets clamp to the boundary. Override per color via autoFlip.
inferRole true Infer each color's role from its name when no explicit role is set. Set to false to opt out of name-based inference (the base-opposite and foreground-default fallbacks still apply).
contrastLevel 'auto' Manual contrast level, 0100, putting the normal variants on a slider between themselves and the high-contrast ones. 0 reproduces the normal output and 100 the high-contrast output, bit for bit. The HC tier is unaffected. See Manual contrast level.
Method Description
glaze.configure(config) Merge into the global config. Bumps a config version that invalidates theme caches.
glaze.getConfig() Snapshot the current resolved config (shallow copy).
glaze.resetConfig() Reset to defaults (also bumps the version counter).

Themes and standalone color tokens keep a sparse local GlazeConfigOverride and merge the live global at resolve time for omitted fields. Authoring .export(override?) freezes the effective merge at call time; restored instances pin that freeze. pastel is instance-only (theme/token override or per-color) — not set via configure(). contrastLevel is the one field the freeze treats as a live preference: only an instance-authored level is written to the snapshot (why).


Output modes

Control which scheme variants appear in tokens() / tasty() / json() exports:

// Light only
palette.tokens({ modes: { dark: false, highContrast: false } });

// Light + dark (default)
palette.tokens({ modes: { highContrast: false } });

// All four variants
palette.tokens({ modes: { dark: true, highContrast: true } });
// → { light, dark, lightContrast, darkContrast }

Resolution priority (highest first):

  1. A global contrastLevel of 100 — pins highContrast: false, since the normal variants already are the high-contrast ones and the tier would duplicate them. Any other level leaves highContrast alone.
  2. Per-call modes option on tokens / tasty / json.
  3. glaze.configure({ modes }) — global config.
  4. Built-in default: { dark: true, highContrast: false }.

Validation

Invalid definitions throw before resolution when Glaze cannot produce a well-defined dependency graph. Recoverable numeric bounds are clamped. A physically unreachable contrast floor or a potentially misleading contrast/opacity combination emits console.warn and returns the closest available result.

Condition Behavior
contrast without base in a theme color Validation error
contrast pair switching metric ([4.5, { apca: 75 }]) Validation error — a WCAG ratio and an APCA Lc are different scales
Relative tone without base in a theme color Validation error
contrast without base in glaze.color() Anchors against the literal seed (no error)
Relative tone without base in glaze.color() Anchors against the literal seed (no error)
Relative tone overshoots [0, 100] Mirror to the other side of the base (autoFlip on, default), or clamp to the boundary (autoFlip off). If the mirror also overshoots, clamp on the authored side.
tone resolves outside 0–100 Clamp silently
'max' / 'min' without base Allowed — resolves to the scheme's tone extreme (root color)
saturation outside 0–1 Clamp silently
Circular base references Validation error
base references non-existent name Validation error
Shadow bg references non-existent color Validation error
Shadow fg references non-existent color Validation error
Shadow bg references another shadow color Validation error
Shadow fg references another shadow color Validation error
Regular color base references a shadow color Validation error
Shadow intensity outside 0–100 Clamp silently
contrast + opacity combined console.warn
Mix base references non-existent color Validation error
Mix target references non-existent color Validation error
Mix base references a shadow color Validation error
Mix target references a shadow color Validation error
Mix value outside 0–100 Clamp silently
Circular references involving mix colors Validation error
Contrast target physically unreachable console.warn (deduped per (name, scheme, target)); closest passing variant returned

Color math utilities

For advanced use, Glaze re-exports its internal color math.

Conversions

import {
  okhslToLinearSrgb,
  okhslToSrgb,
  okhslToOklab,
  oklabToOkhsl,
  srgbToOkhsl,
  hslToSrgb,
  parseHex,
  parseHexAlpha,
  relativeLuminanceFromLinearRgb,
  contrastRatioFromLuminance,
  gamutClampedLuminance,
} from '@tenphi/glaze';
Function Description
okhslToLinearSrgb(h, s, l) OKHSL (h: 0–360, s/l: 0–1) → linear sRGB tuple.
okhslToSrgb(h, s, l) OKHSL (h: 0–360, s/l: 0–1) → gamma-encoded sRGB tuple (0–1 per channel).
okhslToOklab([h, s, l]) OKHSL → OKLab [L, a, b].
oklabToOkhsl([L, a, b]) OKLab → OKHSL.
srgbToOkhsl([r, g, b]) Gamma sRGB (0–1) → OKHSL.
hslToSrgb(h, s, l) CSS HSL → sRGB tuple.
parseHex(hex) Parse #rgb / #rrggbb to sRGB tuple. Returns null on invalid input.
parseHexAlpha(hex) Parse #rgb / #rrggbb / #rrggbbaa; returns [r, g, b, a?].
relativeLuminanceFromLinearRgb(rgb) WCAG relative luminance from linear sRGB.
contrastRatioFromLuminance(yA, yB) WCAG contrast ratio from two luminances.
gamutClampedLuminance(linearRgb) Relative luminance with channel clamping for out-of-gamut colors.

Format writers

Every writer takes h on 0–360 and s / l / t on 0–1 — the scale every conversion above returns, and the scale resolve() stores in a ResolvedColorVariant. The percentages are an output detail: the writers scale by 100 themselves where the CSS syntax asks for one. (The one thing on a different scale is the pair of tone transfers below, toTone / fromTone, which speak the 0–100 tone axis the authoring API takes.)

import {
  formatOkhsl,
  formatOkhst,
  formatRgb,
  formatHsl,
  formatOklch,
} from '@tenphi/glaze';

formatOkhsl(280, 0.6, 0.95); // 'okhsl(280 60% 95%)'
formatOkhst(280, 0.6, 0.95); // 'okhst(280 60% 95%)'
formatRgb(280, 0.6, 0.95); // 'rgb(238.45 239.95 251.1)'
formatHsl(280, 0.6, 0.95); // 'hsl(232.92 61.87% 95.99%)'
formatOklch(280, 0.6, 0.95); // 'oklch(0.9571 0.015 280)'

So a producer composes with a writer directly, with nothing to rescale in between:

const v = glaze.color('#7A4DBF').resolve().light;
const { h, s, l } = variantToOkhsl(v);

formatOkhst(v.h, v.s, v.t); // 'okhst(298.52 70.41% 45.02%)'
formatOkhsl(h, s, l); // 'okhsl(298.52 70.41% 45.27%)'
formatRgb(h, s, l); // 'rgb(122 77 191)'

A value above 1 can only be pre-2.0 percentage-scale input, so the writers console.warn about it once per writer rather than emit a wrong color quietly.

To attach an alpha component, use glaze.format(variant, format) on a ResolvedColorVariant (which carries the alpha channel) instead of these raw writers.

OKHST tone utilities

import {
  toTone,
  fromTone,
  toneFromY,
  yFromTone,
  okhstToOkhsl,
  okhslToOkhst,
  variantToOkhsl,
  REF_EPS,
} from '@tenphi/glaze';
Function Description
toTone(l, eps?) OKHSL lightness (0–1) → tone (0–100, the authoring scale — divide by 100 for formatOkhst). Defaults to REF_EPS.
fromTone(t, eps?) Tone (0–100) → OKHSL lightness (0–1). Inverse of toTone.
toneFromY(y, eps?) / yFromTone(t, eps?) Same transfer in luminance space (0–1).
okhstToOkhsl({ h, s, t }) OKHST → OKHSL ({ h, s, l }).
okhslToOkhst({ h, s, l }) OKHSL → OKHST ({ h, s, t }).
variantToOkhsl(variant) ResolvedColorVariant (stores t) → { h, s, l } for rendering (alpha stays on the variant).
REF_EPS Reference epsilon (0.05) for the canonical tone axis.

ResolvedColorVariant stores { h, s, t, alpha } (tone, not lightness). Use variantToOkhsl(variant).l to recover OKHSL lightness. See OKHST in Glaze for the model.

Contrast solver

import {
  findToneForContrast,
  findValueForMixContrast,
  resolveContrastForMode,
  resolveContrastForLevel,
  resolveMinContrast,
  apcaContrast,
} from '@tenphi/glaze';
Function Description
findToneForContrast(opts) Binary-search for the tone (0–1) that meets a contrast floor (WCAG or APCA) against a base color. Returns { tone, contrast, met, branch, flipped? }.
findValueForMixContrast(opts) Same, but searches for a mix value (0–1) that meets a contrast floor between a base and a target.
resolveContrastForMode(spec, isHC, polarity?, outerExplicitHC?) Resolves a ContrastSpec to { metric: 'wcag' | 'apca', target } for the requested mode (picks the normal or HC entry of any pair). In HC, applies the metric's auto-enhancement unless outerExplicitHC is set or the inner metric pair carries an explicit HC value: APCA +15 Lc (clamped to 106); WCAG AA → AAA / AA-large → AAA-large (AAA-family and bare numbers unchanged).
resolveContrastForLevel(spec, level, polarity?) Resolves a ContrastSpec — including its outer [normal, highContrast] pair — at a manual contrast level (0–100), interpolating the two numeric targets. Levels 0 / 100 delegate verbatim to resolveContrastForMode; a WCAG-vs-APCA metric switch across the pair has no midpoint and switches at 50.
resolveMinContrast(value) Resolves a MinContrast (WCAG preset or number) to a numeric ratio.
apcaContrast(yText, yBg) APCA Lc magnitude (0–106) for two relative luminances.

Exported constants: APCA_PRESETS, APCA_HC_ENHANCEMENT (15, the Enhanced Level delta), APCA_MAX_LC (106).

findToneForContrast options:

Option Default Description
hue Candidate hue (0–360).
saturation Candidate saturation (0–1).
preferredTone Preferred candidate tone (0–1). Kept if it already meets the target.
baseLinearRgb Base color as linear sRGB tuple.
contrast ResolvedContrast ({ metric, target }).
toneRange [0, 1] Search bounds in tone.
epsilon 1e-4 Convergence threshold.
maxIterations 18 Max binary-search iterations per branch.
initialDirection higher-contrast side Direction to search first ('lighter' or 'darker').
flip false When true, try the opposite direction if the initial one doesn't meet the target. When false, only the initial direction is searched — unmet contrasts pin the result to that direction's extreme.
preferInitial false With flip on and both directions meeting the target, keep initialDirection instead of taking whichever result lands nearer preferredTone. Makes the chosen side independent of the target — what contrastLevel uses to keep a color on one side of its base across the ramp (on the normal passes only; the high-contrast ones never probe). The flip fallback is unaffected.

Result: { tone, contrast, met, branch: 'lighter' | 'darker' | 'preferred', flipped? }. flipped: true indicates the initial direction failed and the opposite direction satisfied the target.