Configuration
Glaze has one global config that every theme, palette, and color token reads at resolve time. Themes and tokens can override part of it with a sparse per-instance GlazeConfigOverride. This page lists every field, how the scheme mapping uses them, what Glaze validates, and what it warns about. For the ideas behind tone windows and adaptation, see Light, dark, and high contrast and Tone and OKHST.
import { glaze } from '@tenphi/glaze';
glaze.configure({
lightTone: [10, 100],
darkTone: [15, 95],
darkDesaturation: 0.1,
states: {
dark: '@media(prefers-color-scheme: dark)',
highContrast: '@media(prefers-contrast: more)',
hdr: '@media(dynamic-range: high)',
},
modes: { dark: true, highContrast: false, hdr: false },
shadowTuning: { objectInfluence: 0.5 },
glowTuning: { objectInfluence: 0.5, headroom: 0 },
autoFlip: true,
inferRole: true,
contrastLevel: 'auto',
});| Function | Returns | Description |
|---|---|---|
glaze.configure(config) |
void |
Merges a GlazeConfig into the global config and invalidates every theme, palette, and token cache. |
glaze.getConfig() |
GlazeConfigResolved |
A snapshot of the current global config. |
glaze.resetConfig() |
void |
Restores the defaults and invalidates every cache. |
- Omitted fields keep their current value.
configure()never clears a field by omission, and passingundefinedis the same as omitting it.nullis not: anulltone window throws. UseresetConfig()to start over, orcontrastLevel: 'auto'to leave manual contrast mode. statesandmodesmerge field by field:configure({ modes: { highContrast: true } })keepsmodes.dark.lightTone,darkTone,shadowTuning, andglowTuningare replaced as a whole.configure({ shadowTuning: { tint: 0.3 } })followed byconfigure({ shadowTuning: { reach: 0.9 } })leaves{ reach: 0.9 }, and aglowTuningwithoutheadroomdrops an earlier one. They are copied, so changing the array or object you passed afterwards has no effect.contrastLevel,darkDesaturation, the tone windows, and the tunings are validated on the spot.contrastLevelclamps to 0–100 anddarkDesaturationto 0–1. A non-finite number throws, and so does any other value that isn’t a number (besides the level’s'auto'), such as the string'50'from an<input>: for exampleglaze: contrastLevel must be a finite number 0–100 or 'auto' (got NaN).Window rules are under Tone windows,headroomrules under Glow headroom. A call that throws changes nothing, not even the fields that were valid, and prints no warning: aheadroomclamp found on the way is reported only once the whole call has passed.- Every call bumps an internal config version, so cached resolutions recompute on the next output call. Existing themes pick up the change for every field their own override doesn’t set.
Returns a GlazeConfigResolved — every field present, defaults filled in:
glaze.getConfig();
// → {
// lightTone: { lo: 10, hi: 100, eps: 0.05 },
// darkTone: { lo: 15, hi: 95, eps: 0.05 },
// darkDesaturation: 0.1,
// states: {
// dark: '@media(prefers-color-scheme: dark)',
// highContrast: '@media(prefers-contrast: more)',
// hdr: '@media(dynamic-range: high)',
// },
// modes: { dark: true, highContrast: false, hdr: false },
// autoFlip: true,
// pastel: false,
// inferRole: true,
// contrastLevel: 'auto',
// }The default windows come back in the { lo, hi, eps } form; a window you set comes back in the form you passed ([12, 98] stays [12, 98]), with its endpoints clamped to 0–100. shadowTuning and glowTuning are absent until you set them. pastel is always false here because it is instance-only.
The snapshot is a deep copy: mutating it, nested states, modes, windows, and tunings included, changes nothing. Use configure() to change the config.
For the config a particular theme resolves with (global merged with its override), use theme.getConfig().
The input to glaze.configure(). Every field is optional.
| Field | Type | Default | Description |
|---|---|---|---|
lightTone |
ToneWindow |
[10, 100] |
Light-scheme tone window. Bypassed by high-contrast variants and mode: 'static' colors; widened by contrastLevel. Endpoints clamp to 0–100; an inverted or non-finite window throws. |
darkTone |
ToneWindow |
[15, 95] |
Dark-scheme tone window. Same bypasses, widening, and validation. |
darkDesaturation |
number |
0.1 |
Fraction (0–1) of saturation removed in dark and darkContrast. Clamps to 0–1; non-finite numbers throw. See Saturation. |
states.dark |
string |
'@media(prefers-color-scheme: dark)' |
State key for dark variants in tasty() output. |
states.highContrast |
string |
'@media(prefers-contrast: more)' |
State key for high-contrast variants in tasty() output. |
states.hdr |
string |
'@media(dynamic-range: high)' |
State key for the HDR values of glows in tasty() output. Used only with modes.hdr. Experimental, like every CSS HDR tier. |
modes.dark |
boolean |
true |
Emit dark variants. See Output modes. |
modes.highContrast |
boolean |
false |
Emit high-contrast variants. Independent of contrastLevel, except that a global level of 100 drops the tier (it would duplicate the normal one). |
modes.hdr |
boolean |
false |
Emit the HDR values of glows past SDR white next to their SDR values, in every exporter. Off, the output is the same as with no headroom set. css() reads its own hdr option, which falls back to this. The CSS tiers (css(), tasty(), tailwind()) are experimental, so prefer turning it on per call, such as theme.dtcg({ modes: { hdr: true } }). See HDR output. |
shadowTuning |
ShadowTuning |
— | Default tuning for every shadow color. A per-color tuning merges over it field by field. Fields and defaults: LightingTuning. A headroom here throws. |
glowTuning |
GlowTuning |
— | Default tuning for every glow color. Same shape and merge rule, plus headroom. |
autoFlip |
boolean |
true |
Default for each color’s autoFlip. When on, a contrast floor that can’t be met on the requested side of the base may be met on the other side, and a tone delta that overshoots [0, 100] mirrors to the other side of its base. When off, only the requested direction is used: an unmet floor pins the tone to that direction’s extreme (with a warning) and an overshooting tone delta clamps. Mix colors have no per-color autoFlip and always use this value. |
inferRole |
boolean |
true |
Infer a color’s role (APCA polarity) from its name when it has no explicit role. With false, the base-opposite and 'text' fallbacks still apply. Theme colors only: a standalone token never infers from its name. See How a role is chosen. |
contrastLevel |
number | 'auto' |
'auto' |
Manual contrast level, 0–100. Resolves the light / dark variants at that point between normal (0) and high contrast (100); lightContrast / darkContrast stay the true high-contrast resolution. Numbers clamp to 0–100; a non-finite number or any other non-number throws. Full rules: Manual contrast level. |
pastel is not part of GlazeConfig: it can only be set per instance or per color.
What glaze.getConfig() and theme.getConfig() return: the same fields with defaults applied, plus pastel.
| Field | Type |
|---|---|
lightTone, darkTone |
ToneWindow |
darkDesaturation |
number |
states |
{ dark: string; highContrast: string; hdr: string } |
modes |
{ dark: boolean; highContrast: boolean; hdr: boolean } |
shadowTuning |
ShadowTuning | undefined |
glowTuning |
GlowTuning | undefined — headroom already clamped to 0–2. |
autoFlip |
boolean |
inferRole |
boolean |
contrastLevel |
number | 'auto' |
pastel |
boolean — always false globally; the override’s value on theme.getConfig(). |
type ToneWindow =
| [number, number] // [lo, hi]
| { lo: number; hi: number; eps: number }
| false;lo and hi are OKHSL lightness boundaries (0–100). The authored tone scale 0–100 is spread linearly across the tone interval between them.
| Form | Meaning |
|---|---|
[lo, hi] |
Boundaries at the reference eps 0.05. The common form. |
{ lo, hi, eps } |
Boundaries plus a per-scheme render eps. Advanced; see Reference and render epsilon. |
false |
The full range [0, 100] at the reference eps. Removes the boundaries, not the tone transfer. |
| Window | Default | Stored tone range |
|---|---|---|
lightTone |
[10, 100] |
4.95–100 |
darkTone |
[15, 95] |
9.25–95.9 |
- High contrast replaces both windows with
[0, 100], keeping the window’seps. contrastLevelmoves each boundary toward[0, 100](formula below). Afalsewindow is already the full range, so it doesn’t change with the level.mode: 'static'colors skip the window entirely.- Colors seeded with
fromskip the light window so the light variant reproduces the literal; dark and high contrast still adapt. Seefrom.
Windows are validated where they enter: by configure() on the spot, and for a per-instance override when the instance resolves. Endpoints must be finite numbers with lo ≤ hi, and an object window’s eps must be a finite number above 0 and at most 1e6 (past that the tone transfer loses precision, and from about 1e15 it renders NaN). Anything else throws, null included, in configure() as in an override (messages under Errors). Endpoints outside 0–100 then clamp into it, keeping the form you wrote: [-10, 120] is stored as [0, 100]. lo === hi is allowed and renders every tone at that one lightness.
glowTuning.headroom lets a glow go past SDR white. It counts stops above white, doublings of white’s luminance: 1 allows up to 2× white (tone 121.98), 2 up to 4× (tone 144.34). At the default 0 nothing changes and no variant carries an HDR value. What an HDR glow is for, and where browsers stand: HDR glows.
glaze.configure({ glowTuning: { headroom: 1 } });- Where it’s set. Globally (
glowTuning), per theme (the override’sglowTuning; aglaze.color()token is never a glow), per color (a glow’stuning), or per call (glaze.glow({ tuning })). A glow’stuning.headroommerges over the globalglowTuningfield by field. An override’sglowTuningreplaces the global object as a whole, soglaze(280, 80, { glowTuning: { tint: 0.1 } })drops a globalheadroom, and so does a child’sglowTuninginextend(). - When it applies. Only to a glow whose target tone passes 100 while its SDR value is pinned at its brightest; the exact conditions are in When a glow gets an HDR value. Such a variant gains
hdr, an opaque base plus a gain in stops; seeResolvedHdr. Every scheme uses the same headroom, high contrast included. - Validation. It must be a finite number ≥ 0; anything else throws. Above 2 it clamps to 2 with a warning, deduplicated per field and value and printed only once the whole
configure()call, instance override, or color map has passed. AheadroomonshadowTuning, a shadow’stuning, orglaze.shadow()throws: a shadow darkens toward black, and nothing goes below black. - Export.
export()freezesglowTuningwith the clamped headroom, and a glow’s owntuning.headroomtravels with its definition. Snapshots stay at version 2. - Output. The exporters emit HDR values only with
modes.hdr; see HDR output.
A GlazeConfigOverride overrides resolve-relevant fields for one theme or color token. It is accepted by:
| Call | Effect |
|---|---|
glaze(hue, saturation, config) / glaze(seed, config) |
The theme’s override. See Themes. |
glaze.color(input, config) |
The token’s override. See Color tokens. |
theme.extend({ config }) |
Shallow-merged over the parent’s override for the child theme. |
theme.export(override), token.export(override), palette.export(override) |
Merged into the frozen snapshot config only; the live instance is unchanged. |
| Field | Type | Falls back to | Description |
|---|---|---|---|
lightTone |
ToneWindow |
global lightTone |
Light tone window, or false for the full range. Validated and clamped like the global one when the instance resolves. |
darkTone |
ToneWindow |
global darkTone |
Dark tone window, or false for the full range. Validated the same way. |
darkDesaturation |
number |
global darkDesaturation |
Dark saturation reduction (0–1). Clamps to 0–1 when the instance resolves; non-finite numbers throw. |
autoFlip |
boolean |
global autoFlip |
Default for each color’s autoFlip. |
inferRole |
boolean |
global inferRole |
Name-based role inference. |
contrastLevel |
number | 'auto' |
global contrastLevel |
Manual contrast level for this instance. 'auto' opts out of a global level. A per-instance level never changes which modes are emitted. Validated when the instance resolves. |
shadowTuning |
ShadowTuning |
global shadowTuning |
Replaces the global shadow tuning as a whole for this instance. Per-color tuning still merges over it field by field. Meaningful for themes; a token has no shadows, but a headroom here still throws when it resolves. |
glowTuning |
GlowTuning |
global glowTuning |
Same for glows, headroom included: an override without headroom drops the global one. Validated when the instance resolves. |
pastel |
boolean |
false |
Instance-wide pastel default for colors that don’t set their own. Instance-only: never read from the global config. |
states and modes are not part of the override. They are global, or per call on the output methods.
- Sparse and live. An instance stores only the fields you set. At resolve time they are merged over the current global config, so fields you didn’t set keep following
glaze.configure(). - Copied on creation. The theme or token copies the override when it is created (and
extend()copies the child’s), so changing the object or array you passed afterwards has no effect. Fields are read by property access, so a class instance with getters, inherited fields, or a proxy works as an override. Validation still waits for the resolve. - Overridden fields are pinned. A later
configure()can’t change them for this instance. extendinherits the parent’s override and shallow-merges the child’s on top.- Export freezes.
.export(override?)writesgetConfig() ∪ instance override ∪ overrideinto the snapshot, andthemeFrom/colorFrom/paletteFrompin that freeze as the restored instance’s override. Nested objects (windows, tunings) are replaced wholesale, not merged.statesandmodesare never frozen. contrastLevelis frozen only when authored. A level set on the instance or passed to.export()is written to the snapshot; a level inherited from the global config is a live preference and is left out, so a restored theme keeps following the global slider.
import { glaze } from '@tenphi/glaze';
const t = glaze(280, 80, { lightTone: [0, 50] });
t.colors({ text: { tone: 50, saturation: 1 } });
t.resolve().get('text')!.light.t;
// → 0.2534… — inside [0, 50], whatever the global lightTone
t.resolve().get('text')!.dark.s;
// → 0.72 (darkDesaturation 0.1, not overridden)
glaze.configure({ darkDesaturation: 0.5 });
t.resolve().get('text')!.dark.s;
// → 0.4 — follows the global change
const child = t.extend({ config: { darkTone: false } });
child.getConfig().lightTone; // → [0, 50] (inherited)
child.getConfig().darkTone; // → false (added)// Standalone token without a dark window: light keeps the input tone,
// dark is its exact inversion (100 − T)
glaze.color('#26fcb2', { darkTone: false });
// Restore the #000 → white dark flip with a full-height dark window
glaze.color('#000000', { lightTone: false, darkTone: [15, 100] });
// Structured token with an override
glaze.color(
{ hue: 152, saturation: 95, tone: 74 },
{ darkTone: false, pastel: true },
);mode on a regular color (AdaptationMode) selects which steps of the scheme mapping apply to it. What each mode is for, with visuals: Adaptation modes.
| Mode | Inverted in dark | Tone windows | darkDesaturation, dark seed |
|---|---|---|---|
'auto' (default) |
Yes, in dark and darkContrast |
Applied | Applied |
'fixed' |
No | Applied | Applied |
'static' |
No | Skipped | Skipped |
Tone deltas ('+N' / '-N') are measured from the base. Under 'fixed' and 'static' the delta is applied to the base’s tone in each scheme, sign kept. Under 'auto' it is applied as is in light and lightContrast. In dark, Glaze steps from the base’s anchor, where the base sits on the authored scale, then inverts and remaps the result like an authored tone:
| Base | Anchor |
|---|---|
An 'auto' base placed by a tone (absolute, from, 'max' / 'min' without a base) |
Its authored tone. |
An 'auto' tone delta |
Its base’s anchor plus the delta. |
An 'auto' base with no tone or contrast of its own |
Its base’s anchor. |
Any other base: 'fixed' or 'static', or an 'auto' base placed by a contrast floor, a mix, or a 'max' / 'min' on a base |
Its own dark tone, read back through the dark window. |
At the reference dark eps, every dark step is then the dark window’s scale times the delta, about 0.87× at the defaults ('-20' steps +17.3 on tone 97 and on tone 50 alike), and every darkContrast step is exact, under any light eps. The dark window sets their size, and a custom dark eps reshapes them in dark and darkContrast alike: a dark delta lands where the same authored tone would. A 'fixed' or 'static' base doesn’t invert, but the 'auto' dependent does, around wherever the base sits: '+10' on a fixed tone-30 base steps +10 in light and −8.7 in dark. Near the ends of the scale, dark has two clamps of its own (a step off the scale, a base past the dark window), listed under Tone values. Concept: How relative tone adapts.
Every scheme runs the same tone pipeline; there is no fitted dark curve. The transfer functions are specified in Tone and OKHST and the canonical OKHST specification.
For an authored tone T (0–100) and the scheme’s active window { lo, hi, eps }:
T' = 100 − T if mode is 'auto' and the scheme is dark
= T otherwise
loT = toTone(lo / 100, eps)
hiT = toTone(hi / 100, eps)
l = fromTone(loT + (T' / 100) × (hiT − loT), eps) // OKHSL lightness
t = toTone(l, REF_EPS) // stored tonemode: 'static' skips all of it: t = clamp(T, 0, 100). The stored t is what ResolvedColorVariant.t holds (divided by 100), what tone deltas are measured against, and what the contrast solver searches.
The active window per variant:
| Variant | Window |
|---|---|
light |
lightTone (widened by contrastLevel) |
dark |
darkTone (widened by contrastLevel) |
lightContrast, darkContrast |
[0, 100], with the configured window’s eps |
With a numeric contrastLevel L and f = L / 100, the normal windows move toward the full range:
lo(f) = lo × (1 − f)
hi(f) = hi + (100 − hi) × fThe defaults at level 50 are light [5, 100] (stored tone 1.63–100) and dark [7.5, 97.5] (stored tone 3.15–97.97). eps is not interpolated.
Authored tones under the default windows (neutral seed), as stored tone:
| Authored | Mode | light |
dark |
lightContrast |
darkContrast |
|---|---|---|---|---|---|
| 0 | auto |
4.95 | 95.9 | 0 | 100 |
| 50 | auto |
52.47 | 52.57 | 50 | 50 |
| 100 | auto |
100 | 9.25 | 100 | 0 |
| 0 | fixed |
4.95 | 9.25 | 0 | 0 |
| 100 | fixed |
100 | 95.9 | 100 | 100 |
| 50 | static |
50 | 50 | 50 | 50 |
A render eps other than the reference bends mid tones: with lightTone: { lo: 10, hi: 100, eps: 0.2 }, authored tone 50 stores 63.89 in light (instead of 52.47) and 62.95 in lightContrast.
S_dark = S_light × (1 − darkDesaturation) // dark and darkContrast- Skipped for
mode: 'static'. - Bypassed when the dark saturation is authored: always for a per-color
darkSaturation, and for a themedarkSaturationseed on every color that reads the seed. Afromcolor without its ownsaturationordarkSaturationstarts from the literal’s saturation instead, so a seeddarkSaturationdoesn’t bypass its reduction. See Dark seed. darkDesaturationclamps to 0–1 where it is set, so it can remove at most all of a color’s saturation and never adds any:2acts as1(dark variants go gray) and-0.5as0. A non-finite value throws.
With seed saturation 80 and the default 0.1, an auto color’s saturation is 0.8 in light and 0.72 in dark and darkContrast; a static one stays 0.8.
Hue isn’t mapped. Dark variants re-anchor relative hues to the theme’s darkHue seed and use a per-color darkHue when one is set. An absolute per-color hue (or a from color’s literal hue, when it has no hue or darkHue) is kept as is, and static colors ignore both. See Dark seed and darkHue / darkSaturation.
Glaze validates a theme’s definitions when it resolves — on the first resolve() or output call after the definitions change, not when you call colors(). Graph errors throw; recoverable numeric bounds are clamped; a physically unreachable target pins to the limit of its search and warns; risky combinations resolve and warn.
| Condition | Message |
|---|---|
contrast without base (theme color) |
glaze: color "a" has "contrast" without "base". |
Relative tone without base (theme color), in either entry of a pair ([50, '+20'] included) |
glaze: color "a" has relative "tone" without "base". |
No absolute tone, no from, and no base |
glaze: color "a" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent). |
from can’t be parsed |
glaze: color "a" has an invalid "from" value. glaze: invalid hex color "#zzz". |
Malformed contrast, regular or mix: a misspelled preset ('aa', { apca: 'Body' }), a non-finite number, an object without exactly one of wcag / apca set (a key holding undefined is unset; other keys are ignored), or a pair without two entries |
glaze: color "a" has an invalid "contrast" "aa". Use a WCAG ratio or 'AA' / 'AAA' / 'AA-large' / 'AAA-large', { wcag: … }, { apca: … }, or a [normal, highContrast] pair of these. |
A color field the resolver can’t place: a tone, hue, darkHue, saturation, darkSaturation, mix value, or shadow / glow intensity that isn’t a finite number, a relative string that isn’t '+N' / '-N' ('+-5'), or an opacity outside 0–1 |
glaze: color "a" has an invalid "tone" NaN. |
A theme seed hue / saturation (or a given darkHue / darkSaturation) that isn’t a finite number, at glaze() |
glaze: theme seed saturation must be a finite number (got undefined). |
contrast pair switches metric ([4.5, { apca: 75 }]), regular or mix |
glaze: color "a" has a "contrast" pair that switches metric (wcag → apca). A WCAG ratio and an APCA Lc are different scales; use one metric for both entries, e.g. { wcag: [normal, highContrast] }. |
base names a missing color |
glaze: color "a" references non-existent base "nope". |
base names a shadow or glow |
glaze: color "b" base "a" references a glow color. |
Shadow / glow without a bg |
glaze: shadow "a" is missing "bg". (glaze.shadow() / glaze.glow(): glaze.shadow(): "bg" is required.) |
Shadow / glow bg or fg names a missing color |
glaze: shadow "a" references non-existent bg "nope". |
Shadow / glow bg or fg names a shadow or glow |
glaze: glow "b" bg "a" references a shadow color. |
Mix base names a missing color |
glaze: mix "a" references non-existent base "nope". |
Mix target names a missing color |
glaze: mix "a" references non-existent target "nope". |
Mix base / target names a shadow or glow |
glaze: mix "a" target "sh" references a shadow color. |
Circular reference through base, bg, fg, or a mix’s base / target |
glaze: circular base reference detected involving "a". |
Non-finite or non-number contrastLevel other than 'auto', null included (thrown by configure(), or when an instance with such an override resolves) |
glaze: contrastLevel must be a finite number 0–100 or 'auto' (got NaN). |
Non-finite or non-number darkDesaturation (same two places) |
glaze: darkDesaturation must be a finite number 0–1 (got NaN). |
Tone window with lo above hi (same two places, for lightTone or darkTone) |
glaze: lightTone lo (90) is above hi (20). A window runs from its dark end to its light end: [lo, hi] with lo ≤ hi, e.g. [20, 90]. |
| Tone window endpoint that isn’t a finite number | glaze: lightTone lo must be a finite number (got NaN). |
Object tone window whose eps isn’t a finite number above 0 and at most 1e6 (missing included) |
glaze: lightTone eps must be a finite number above 0 and at most 1e6 (got 0). |
Tone window that isn’t [lo, hi], { lo, hi, eps }, or false (null included) |
glaze: lightTone must be [lo, hi], { lo, hi, eps }, or false (got [10]). |
Glow headroom that isn’t a finite number ≥ 0: in glowTuning (same two places), a glow’s tuning (at resolve), or glaze.glow() |
glaze: glowTuning.headroom must be a finite number ≥ 0 — stops above SDR white (got -1). The prefix names the source (glow "g" tuning, glaze.glow() tuning), and the value is shown as written: (got "2"), (got [1]). |
objectInfluence, tint, reach, or hueBleed set to something other than a finite number, in any tuning |
glaze: shadowTuning.reach must be a finite number (got NaN). (prefixed like headroom) |
headroom in shadowTuning, a shadow’s tuning, or glaze.shadow() |
glaze: shadowTuning.headroom is not supported — a shadow darkens toward black, and nothing goes below black. Headroom is for glows (glowTuning or a glow's tuning). |
Every place that takes a GlazeColorValue — a theme color’s from, glaze.color(), and the bg / fg of glaze.shadow() / glaze.glow() — uses one parser. It throws immediately for standalone calls; for from the message is wrapped in the color’s name (above).
| Input | Message |
|---|---|
| Hex of the wrong length (not 3, 4, 6, or 8 digits) or with non-hex digits | glaze: invalid hex color "#zzz". |
A string that isn’t hex or rgb() / rgba() / hsl() / hsla() / okhsl() / okhst() / oklch() (named colors included) |
glaze: unsupported color string "red". |
| A color function without exactly 3 components (alpha aside) | glaze: expected 3 components in "rgb(1 2)". |
A color-function component that isn’t a number (or none), or has a unit it can’t take |
glaze: invalid component "abc" in "hsl(abc 50% 50%)". |
{ h, s, l } with a non-finite field |
glaze.color: OkhslColor h/s/l must be finite numbers. |
{ h, s, l } with s or l above 1.5 |
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, s, l }, { h, s, t }, or { l, c, h } with a 0–1 field below 0 |
glaze.color: OkhslColor s must be 0 or more (got -0.1). (OkhstColor / OklchColor likewise) |
{ h, s, t } with a non-finite field, or s / t above 1.5 |
glaze.color: OkhstColor h/s/t must be finite numbers. / glaze.color: OkhstColor s/t must be in 0–1 range. … |
{ r, g, b } with a channel outside 0–255 |
glaze.color: RgbColor r must be a finite number in 0–255 (got 300). |
{ l, c, h } with a non-finite field, or l / c above 1.5 |
glaze.color: OklchColor l/c/h must be finite numbers. / glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings). |
An [r, g, b] array |
glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead. |
Errors specific to the structured glaze.color() input, restoring snapshots, and building palettes are listed on Color tokens, Themes, and Palettes.
| Condition | Behavior |
|---|---|
tone: 'max' / 'min' |
A root color at the scheme’s tone extreme. |
contrast without base in glaze.color() |
Measured against the token’s own input color. |
A tone delta (tone: '+N', or in either entry of a pair) without base in glaze.color() |
Measured from the token’s own input color. |
| Value | Range |
|---|---|
Resolved tone |
0–100 |
Per-color saturation / darkSaturation factor |
0–1 |
Shadow / glow intensity |
0–100 (an unreachable step then warns) |
Lighting tint, reach, hueBleed |
0–1. objectInfluence isn’t clamped; the object factor k floors at 0. |
Mix value |
0–100 |
contrastLevel |
0–100 |
darkDesaturation |
0–1 |
Tone window lo / hi |
0–100 (after the lo ≤ hi check) |
| WCAG numeric target | At least 1 |
| APCA numeric target | Its magnitude (-60 → 60) |
A tone delta that overshoots [0, 100] mirrors to the other side of its base when autoFlip is on; if the mirror also overshoots, it clamps on the authored side. With autoFlip: false it clamps on the authored side. For example, a 'static' color with tone: '-20' on a static base at tone 10 resolves to 30 with autoFlip and to 0 without, in every scheme; with '-60' on a base at 50 it resolves to 0 either way. An 'auto' child gets the same results in light. In dark it mirrors around wherever the base sits, by the anchor rule in Adaptation modes: with autoFlip, the +20 it took in light turns into a step down from tone 10, runs off the scale, and stops at the dark window’s edge, 9.25; without it, the '-20' becomes 17.3 tones up, 27.3. The flip is always decided in light, and dark mirrors whichever side light took, with two dark-only clamps (a step off the scale, a base past the dark window) listed under Tone values.
| Condition | Result |
|---|---|
A regular color’s contrast floor is out of reach |
Pins to the extreme of the side searched first; contrast unmet warning. |
A mix’s contrast floor is out of reach |
Pins to the end of the base → target range it searched; mix contrast unmet warning. |
| A shadow / glow step is out of the pigment’s reach | Pins to the pigment’s strongest composite (or stays transparent if it can’t move the surface at all); lighting warning. A glow whose HDR value covers the step doesn’t warn. In high contrast and at a contrastLevel other than 0, only a step out of reach in both light and dark warns. |
A glow step past white that its headroom cap leaves short |
The HDR value pins at the cap, 2^headroom on the brightest linear channel; the HDR form of the lighting warning. |
A glow headroom above 2 |
Clamps to 2; headroom clamped warning. |
contrast and opacity on the same color |
Resolves the floor for the opaque color; contrast and opacity warning. |
A color string with an alpha component (#rrggbbaa, rgb(… / a)) where Glaze has no opacity field |
Alpha dropped; alpha dropped warning. |
An unmet mix normally pins to the pure target. A mix whose value is already 100, including the high-contrast entry of value: [80, 100], can only search toward the base, so when its pure target misses the floor it pins to the pure base instead. A pure target that clears the floor, even inside the solver’s margin, is kept.
Glaze reports problems it can recover from through console.warn, always prefixed with glaze:. Nothing is thrown.
| Warning | Emitted when | Deduplicated |
|---|---|---|
| Contrast unmet | The solver can’t reach a regular color’s floor in a scheme. | Yes |
| Mix contrast unmet | The solver can’t reach a mix’s floor in a scheme. | Yes |
| Contrast drift | A chromatic color the solver reported as meeting its floor measures under it after resolution. | Yes |
| Shadow or glow out of reach | A shadow / glow step exceeds what its pigment can do, or a glow’s headroom cap leaves its step short. In high contrast and at a contrastLevel other than 0: only when that holds in both light and dark. |
Yes |
| Headroom clamped | A glow headroom above 2 is clamped. |
Yes |
| Contrast and opacity | A regular color has both contrast and opacity. |
No — once per resolve |
| Alpha dropped | A color value carries alpha that Glaze discards. | Once per color string |
| Percentage scale | A format* writer gets a value above 1. |
Once per writer |
| Palette collision | Two palette themes produce the same token name. See Collision detection. | No |
The first five share one session cache of 256 keys, which starts over when full. A key includes the color name, scheme, target, and measured value (for a clamp, the field and value), so a dev server that re-resolves on every change prints each distinct problem once. Resolved themes are cached until their definitions or the config change, so the contrast-and-opacity warning prints once per actual resolve. The alpha-dropped warning keeps its own cache of 256 color strings, so it prints once per string.
glaze: color "badge-text" cannot meet WCAG 7.00 in light scheme
(got 4.94).
Try widening the tone window, lowering the contrast target, or picking
a base color further from this color's tone.Fires when the result is more than 2% under a WCAG target, or more than 1.5 Lc under an APCA target (the solver aims slightly past the target, so smaller shortfalls are rounding). The scheme is light, dark, lightContrast, or darkContrast; APCA targets print as APCA Lc 75.0. Standalone tokens print their name option, or "value" when it isn’t set. Fixes: When a floor can’t be met.
glaze: mix "accent-wash" cannot meet WCAG 4.50 against its base in
light scheme (got 3.54).
Even the pure target falls short; try lowering the contrast target or
mixing toward a color further from the base.This one is for accent-wash: { type: 'mix', base: 'surface', target: 'accent', value: 10, contrast: 'AA' } in glaze(280, 80), with surface at tone 97 and accent at tone 55. It uses the same margins and scheme labels as contrast unmet, measured between the mix and its base. got is the contrast of the value the mix settled at: the pure target, or the pure base when the value is already 100 (got 1.00, or 0.00 for an APCA floor). Fixes: Contrast on mixes.
When the pastel cap is why a mix misses (its target is a standard color outside the pastel gamut that would meet the floor painted in full), the hint names the cap:
Even the pure target falls short once the mix's pastel gamut caps its
saturation; try `pastel: false` on the mix, lowering the contrast
target, or mixing toward a color further from the base.See Pastel mixes.
glaze: color "link" drifts below WCAG 4.50 in light scheme
(measured <ratio>).
Chromatic luminance differs from the gray tone; nudge the tone or
saturation if the floor matters.After all schemes resolve, Glaze re-measures every regular color with contrast and base against its base, in the floor’s metric and the color’s role polarity, with the same margins. It skips a variant whose floor the solver reported as unmet, which already has its contrast unmet warning, and an achromatic variant (saturation 0), whose tone is exact. The solver measures the real chromatic color, so a floor it meets passes this check too: the advisory is a safety net and normally stays silent. Mixes are not re-measured; an unmet mix has its own warning. At an effective contrastLevel of 100, global or per instance, the high-contrast slots are skipped because they mirror the normal ones. Background: Chromatic drift advisory.
glaze: glow "g" cannot reach a 5.0-tone step in light scheme
(reached 0.0 at full strength).
Lower the intensity, move the surface away from white, or raise a
tuned-down `reach`.Fires when the composite lands more than 0.05 tones short of intensity × k. The standalone functions name themselves instead of a color and omit the scheme: glaze: glaze.glow() cannot reach a 5.0-tone step (reached 0.0 at full strength). …. A shadow’s message suggests moving the surface away from black. See Unreachable steps.
Which schemes can fire it:
light/darkatcontrastLevel'auto'or0: every shortfall, as soon as that scheme resolves.lightContrast/darkContrast, andlight/darkat any other level: only when the same pair falls short in both light and dark (lightContrastanddarkContrast, orlightanddarkat that level). These passes move surfaces past the authored windows — high contrast bypasses them, a level widens them — so a white page becomes black indarkContrastand no step fits it. One that fits the other scheme pins without a warning. These are checked after all four schemes resolve. See When Glaze moves the surface.
With a headroom, a glow whose HDR value covers the step prints nothing. One the cap leaves short gets this form instead, here for a 30-tone glow on a tone-100 surface at headroom: 1:
glaze: glow "big" cannot reach a 30.0-tone step in light scheme even
with 1 stop of headroom (reached 22.0).
Lower the intensity or raise `headroom`.At headroom: 2 the advice reads Lower the intensity; `headroom` is already at its 2-stop maximum. A glow without an HDR value, such as one whose pigment can’t lift a non-white surface at all (reach: 0 on a gray surface), keeps the first form; see When a glow gets an HDR value.
glaze: glowTuning.headroom 3 is above the 2-stop maximum; using 2.
Headroom counts stops (doublings of SDR white's luminance), not a
ratio: 1 stop is 2× white, 2 stops 4×.A headroom above 2 is likely a ratio typed where stops were meant (4 for “4× white”). The prefix names where the value came from: glowTuning, glow "g" tuning, or glaze.glow() tuning. Printed once per field and value, and only after the whole configure() call, instance override, or color map has validated, so a call that throws on another field leaves the warning for the corrected retry.
glaze: color "text" has both "contrast" and "opacity".
Opacity makes perceived tone unpredictable.Emitted during validation. For a translucent color with a floor, use a transparent mix.
glaze: alpha component dropped from "#ffffff80"; use the color's
`opacity` (or a transparent mix) for translucency.Any color string Glaze parses — a from value, a glaze.color() value, or a glaze.shadow() / glaze.glow() input — keeps only its color. Use opacity on a regular color, or a transparent mix, for translucency.
glaze: formatRgb() got a value above 1 — it takes the 0-1 factors every
Glaze producer returns (resolve(), variantToOkhsl, srgbToOkhsl), not
0-100 percentages.
Divide by 100, or drop the "* 100" left over from the pre-2.0 signature.The format writers take s / l / t on 0–1. A value above 1 can only be pre-2.0 percentage input, so each writer warns the first time it sees one in the process (and still formats the value). Upgrade notes: 2.0 — format* takes 0–1.