Color definitions
theme.colors(defs) takes a ColorMap: color names mapped to definitions. This page lists every definition shape and field. For why the fields behave as they do, follow the links to the concept pages: Tone and OKHST, Light, dark, and high contrast, Contrast solving, Shadows and glows, and Mixes and transparency.
import type {
ColorDef,
ColorMap,
RegularColorDef,
ShadowColorDef,
GlowColorDef,
MixColorDef,
} from '@tenphi/glaze';type ColorDef =
| RegularColorDef
| ShadowColorDef
| GlowColorDef
| MixColorDef;
type ColorMap = Record<string, ColorDef>;The type field picks the shape:
type |
Shape | Produces | References |
|---|---|---|---|
| omitted | RegularColorDef |
An opaque color, or a fixed opacity |
base |
'shadow' |
ShadowColorDef |
A translucent dark overlay | bg, fg |
'glow' |
GlowColorDef |
A translucent light overlay | bg, fg |
'mix' |
MixColorDef |
A blend of two colors, opaque or translucent | base, target |
Rules shared by all four:
- A reference names another color in the same theme. A missing name, or a cycle through
base/bg/fg/target, throws when the theme first resolves. - Shadows and glows are leaves: no color can reference one, and they can’t reference each other.
- Colors resolve in dependency order, so you can define them in any order.
- Every shape accepts
pastelandinherit.
Every validation rule and its error is listed in Validation, and every warning in Warnings.
A color placed by tone, either on its own (a root) or relative to another color (a dependent).
| Field | Type | Default | Description |
|---|---|---|---|
from |
GlazeColorValue |
— | Seed the color from a literal value. Supplies the hue, the tone, and an absolute saturation. See from. |
tone |
HCPair<ToneValue> |
from’s tone, else the base’s tone |
Number: absolute tone 0–100. '+N' / '-N': a tone delta from base (requires base). 'max' / 'min': the scheme’s tone extreme. See Tone values. |
saturation |
number |
1 |
Factor (0–1) of the theme’s saturation seed. Clamped to 0–1. With from, it replaces the color’s absolute saturation and is still a factor of the seed. |
hue |
number | RelativeValue |
The seed hue (or from’s hue) |
Number: absolute hue 0–360. '+N' / '-N': relative to the theme seed hue, never to a base. See Hue and saturation. |
darkHue |
number | RelativeValue |
hue |
Hue for the dark and darkContrast variants. A relative value is relative to the theme’s dark seed hue. See Dark seed. |
darkSaturation |
number |
saturation |
Factor (0–1) of the dark saturation seed for the dark variants. When set, darkDesaturation is not applied on top. |
base |
string |
— | Another color in the same theme. Makes this a dependent color. Can’t be a shadow or glow. |
contrast |
HCPair<ContrastSpec> |
— | Contrast floor against base, WCAG or APCA. Requires base. See contrast. |
mode |
AdaptationMode |
'auto' |
How the color adapts between light and dark. See mode. |
autoFlip |
boolean |
The config’s autoFlip (true) |
Mirror out-of-range results to the other side of the base instead of clamping. See autoFlip. |
opacity |
number |
1 |
Fixed alpha, 0–1. See opacity. |
pastel |
boolean |
The theme’s pastel (false) |
Use the hue-independent safe chroma limit for this color. See Per-color pastel. |
role |
RoleInput |
Inferred | How the color is used against base; fixes APCA polarity. See Roles. |
inherit |
boolean |
true |
Whether extend() copies the color into child themes. See inherit. |
import { glaze } from '@tenphi/glaze';
const theme = glaze(280, 80);
theme.colors({
// root
surface: { tone: 97, saturation: 0.1 },
// dependent
'surface-text': { base: 'surface', tone: '-52', contrast: 'AA' },
// HC pair
border: { base: 'surface', tone: ['-8', '-16'] },
// root that keeps its tone in dark
accent: { tone: 52, mode: 'fixed' },
// literal value
brand: { from: '#2f5bff', base: 'surface', contrast: 3 },
});Root or dependent. A color with an absolute tone (a number, 'max', or 'min') or a from value and no base is a root. A color with a base is dependent. Every regular color needs one of the three, or validation throws glaze: color "<name>" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent).
from places a color at a literal value instead of deriving it from the seed: for brand colors, white-label and multi-tenant themes, and imported design tokens, where the value 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 },
});
// brand, light: #2f5bff (it already clears 3:1 on white, so it is kept)It accepts every GlazeColorValue form: hex, rgb(), hsl(), okhsl(), okhst(), oklch(), or a value object. It supplies three things: hue, tone, and, unlike any other theme color, an absolute saturation. Every other color’s saturation is a factor of the seed, so the seed is a ceiling. A from color carries its own chroma, so it can be more saturated than its theme and doesn’t move when the seed changes:
for (const seed of [5, 50, 100]) {
const theme = glaze(280, seed);
theme.colors({ brand: { from: '#2f5bff' } });
// brand, light: #2f5bff at every seed
}| Aspect | Behavior |
|---|---|
| Which variant is exact | The light, normal-contrast variant reproduces the value: the color resolves with a local lightTone: false, the default the value forms of glaze.color() use. Dark and high contrast adapt as usual: those are the variants a reader picks when the normal one doesn’t work for them, so readability outranks fidelity. To pin the value in every scheme, add mode: 'static'. |
contrast |
Still a floor in every scheme. A value that clears it is emitted untouched; one that misses moves only as far as the floor. The floor is solved per scheme, so which schemes come out exact depends on the color. |
tone |
Overrides the tone the value carried. Without tone, the color sits at the value’s tone, even when it has a base. |
hue |
An absolute hue replaces the value’s hue and keeps its saturation and tone. A relative hue is relative to the theme seed hue, as on any color, not to the value’s hue. |
saturation |
Reverts to its usual meaning: a factor of the seed, not of the value. |
| Dark saturation | Without saturation or darkSaturation on the def: starts from the value’s own saturation, and darkDesaturation applies, even when the theme has a dark saturation seed. With a def-level saturation, it is a factor of the dark seed when one is set (no reduction), else of the seed with darkDesaturation. A def-level darkSaturation is a factor of the dark seed, taken literally. |
| Dark hue | The value’s hue, unless hue or darkHue is set. |
base |
Not needed: a from color is a root on its own. Add base + contrast when it has to stay legible against something. |
An unparsable value throws glaze: color "<name>" has an invalid "from" value. followed by the parser’s message.
The concept and a worked example are in Contrast solving.
type RelativeValue = `+${number}` | `-${number}`;
type ExtremeValue = 'max' | 'min';
type ToneValue = number | RelativeValue | ExtremeValue;tone is on the contrast-uniform OKHST axis, 0–100: equal tone differences give equal WCAG contrast for neutrals, and chromatic colors can drift slightly in measured luminance. The model is in Tone and OKHST.
| Form | Example | Meaning |
|---|---|---|
| Number (absolute) | tone: 45 |
Absolute tone 0–100. |
| Delta | tone: '-52' |
Signed difference from the base’s resolved tone. Requires base. |
| Extreme | tone: 'max' / tone: 'min' |
The scheme’s highest ('max' = author tone 100) or lowest ('min' = 0) tone. No base needed. |
| HC pair | tone: ['-7', '-20'] |
[normal, highContrast]. A single value applies to both. |
Absolute tone. On a root, the tone is mapped into each scheme by its mode. On a dependent, it positions the color independently of the base: each scheme maps it on its own, and a contrast floor acts as the safety net.
Delta. Adds a signed tone delta to the base’s resolved tone. In light and lightContrast the step is exact, which gives an equal contrast step for neutrals and a stable visual progression for chromatic colors. Dark depends on the color’s mode:
'auto'(default): Glaze steps from the base’s anchor, where the base sits on the authored scale, and inverts and remaps the result through the dark window, so dark text on a light surface becomes light text on a dark one. The anchor is the base’s authored tone when the base is an'auto'color placed by a tone, and its base’s anchor plus the delta when it is an'auto'delta. For any other base —'fixed'or'static', or an'auto'base placed by a contrast floor, a mix, or a'max'/'min'on a base — it is the base’s own dark tone read back through the dark window, so the dependent mirrors around wherever the base sits ('+10'on a fixed tone-30 base steps −8.7 in dark). Full table: Adaptation modes.'fixed'and'static': the delta is applied to the base’s tone in each scheme, sign kept.
At the reference dark eps, every 'auto' dark step is the dark window’s scale times the delta, about 0.87× at the defaults: on a tone-97 surface, '-10' becomes +8.7 in dark and '-52' becomes +45.1; on a tone-50 base, '+4' becomes −3.5 and '-4' becomes +3.5; a '-4' hover on an 'AAA' button steps +3.5. In darkContrast the same step is exact (±N), under any light eps. A custom dark eps reshapes the dark curve in dark and darkContrast alike, so a dark delta lands where the same authored tone would instead of at scale × delta. Near the ends of the scale the clamps below apply. See How relative tone adapts.
If base + delta falls outside [0, 100]:
- with
autoFlip(the default), the delta is mirrored to the other side of the base; - if the mirrored target is also out of range, or
autoFlipis off, the result is clamped on the authored side.
The flip is decided on the light tone, and dark mirrors whichever side light took. Two clamps apply in dark only:
- Step off the scale. Dark steps from the anchor, so a step that fits from the base’s light tone can still run past 0 or 100 from the anchor; dark then stops at the dark window’s edge. An
'auto'base placed by a tone meets this at the bottom, because the light window lifts authored tone 0 to 4.95:'-4'on tone 2 steps about +1.7 in dark, on tone 0 not at all. A base anchored at its own dark tone can meet it at either end: inglaze(280, 40), a'-4'hover oncontrast: 12text over a tone-97 surface steps +2.3 in dark, not +3.5. IndarkContrastthis applies only to a base anchored at its own dark tone, or under a custom lighteps. - Base past the dark window. A contrast floor or an extreme can push a base past the dark window, and a
'fixed'delta can ride its base past it. A step from it is limited by the window edge or the base, whichever is further out: a step back toward the window moves the full scaled delta, one pointing further out stops at the base. On a tone-97 surface,ink: { base: 'surface', tone: 'min' }sits at tone 100 in dark;'+4'on it steps −3.5 there and'-4'stays at 100.
Add a contrast floor when a dark step must survive either clamp.
A delta needs base. When either entry of tone is a delta and there is no base, validation throws glaze: color "<name>" has relative "tone" without "base". That includes a delta only in the high-contrast entry (tone: [50, '+20']). With a base, the entries can mix forms: tone: [50, '-20'] places the color at absolute tone 50 in the normal schemes and exactly 20 tones from its base in high contrast: below it in lightContrast, and above it in darkContrast under mode: 'auto'.
Extreme without a base. Flows through the scheme mapping like an absolute tone. Under mode: 'auto' it inverts in dark: 'max' is the lightest tone in light and the darkest in dark. Use mode: 'static' to keep the same extreme in every scheme, or mode: 'fixed' to keep the same end without inverting.
Extreme with a base. Keeps the pair’s contrast across schemes instead of squeezing it through the dark window. In dark, Glaze replays the tone shift the light scheme applied between the base and the extreme against the base’s dark tone: mirrored under mode: 'auto', same sign under 'fixed'. The result is clamped to [0, 100] only, so it can cross the darkTone window edge, and pins at 100 or 0 when the shift doesn’t fit. mode: 'static' is unaffected. The rule and a worked example are in Extremes in dark.
No tone. A dependent with base and no tone takes the base’s tone (a delta of 0). A from color takes the value’s tone.
const theme = glaze(280, 80);
theme.colors({
surface: { tone: 97 },
'gradient-end': { tone: 90, hue: '+20' }, // 280 + 20 = 300
warning: { tone: 60, hue: 40 }, // absolute
'muted-fill': { tone: 90, saturation: 0.3 }, // 30% of seed saturation
});- A relative
hueis relative to the theme seed hue, never to a base color. Hues wrap to 0–360. saturationis a 0–1 factor of the theme’s 0–100 saturation seed. Values outside 0–1 are clamped.- A
fromcolor uses its own hue and absolute saturation unless these fields are set; seefrom.
Tone inverts between light and dark, but hue and saturation don’t: by default every scheme uses the one seed, and dark gets the flat darkDesaturation reduction. When dark needs its own chroma, give the theme a dark seed, override single colors, or both:
const theme = glaze({
hue: 280,
saturation: 80,
darkHue: 268, // dark variants seed from 268 instead of 280
darkSaturation: 65, // and from 65 instead of 80
});
theme.colors({
surface: { tone: 97 },
accent: { tone: 55, hue: '+20', darkHue: '+35' },
warning: { tone: 60, saturation: 0.9, darkSaturation: 0.6 },
});Both fields apply to the dark and darkContrast variants.
| Rule | Behavior |
|---|---|
| Units | The seed’s darkSaturation is 0–100, like the theme saturation. A color’s darkSaturation is a 0–1 factor, like a color’s saturation. |
| Fallback | darkHue falls back to hue and darkSaturation to saturation; a color falls back to the seed. Omitting everything gives the single-seed behavior. |
| Relative hue | darkHue: '+N' is relative to the theme’s dark seed hue. A plain hue: '+N' with no darkHue is also re-applied to the dark seed hue, so the whole palette rotates together. |
| Absolute hue | An absolute hue with no darkHue is used as is in dark: absolute means absolute in every scheme. |
darkDesaturation |
Skipped as soon as any explicit dark saturation is authored, on the seed or on the color. The value you write is the value you get. A from color with no saturation or darkSaturation of its own is the exception: a seed-level darkSaturation doesn’t stop its reduction. |
mode: 'static' |
Ignores both. A static color keeps one hue and saturation in every scheme. |
| Shadows, glows, and mixes | Have no channels of their own. They derive hue and saturation from bg / fg / base / target, so they follow dark overrides automatically. |
With splitHue, a dark seed makes the hue custom properties scheme-dependent, and Glaze re-declares them for dark automatically. The concept is in Light, dark, and high contrast.
contrast sets a minimum contrast against base, solved separately in every scheme:
theme.colors({
surface: { tone: 97 },
'surface-text': { base: 'surface', tone: '-52', contrast: 'AA' },
'muted-text': {
base: 'surface',
tone: '-40',
contrast: { apca: 'content' },
},
'accent-text': { base: 'surface', contrast: { wcag: [4.5, 7] } },
});The type is HCPair<ContrastSpec>: a WCAG ratio or preset, or an { apca } Lc target, optionally as a [normal, highContrast] pair. Every form, preset, and the automatic high-contrast escalation are in Contrast and high contrast. How the solver searches tone is in Contrast solving.
autoFlip decides what happens when a result falls outside its valid range:
- Delta overshoot. When
base ± deltaleaves[0, 100], the delta is mirrored to the other side of the base ('+30'becomes'-30') instead of clamping to the boundary. If the mirrored target is also out of range, the original delta is kept and clamped on the authored side. Light decides the side and dark mirrors it, with the dark-only clamps under Tone values. contrastdirection. When the requested direction can’t meet the floor, the solver tries the opposite side of the base and uses it if it passes.
It defaults to the theme’s effective autoFlip, which is true unless changed with glaze.configure() or a config override. Set autoFlip: false on a color to keep a delta on the authored side, or to pin an unmet floor to that direction’s extreme (with a warning) instead of crossing over. Mixes have no per-color autoFlip; their contrast solve follows the theme’s. See Contrast solving.
type AdaptationMode = 'auto' | 'fixed' | 'static';| Mode | In dark |
|---|---|
'auto' (default) |
Tone inverts (100 − t), then maps into the dark tone window. |
'fixed' |
Tone maps into the dark window without inverting. For brand fills, CTAs, and status banners. |
'static' |
The same tone, hue, and saturation in every scheme, with no tone window. |
High-contrast variants use the full tone range in every mode, and 'auto' still inverts in darkContrast. Each mode in every scheme, with visuals and when to use it, is in Light, dark, and high contrast; which mapping steps each mode applies is in Adaptation modes, and the formulas and per-variant tones in Scheme mapping. How each mode treats a tone delta is under Tone values.
A fixed alpha, 0–1. The output color includes it:
theme.colors({
scrim: { tone: 0, saturation: 0, mode: 'static', opacity: 0.5 },
});
// scrim: oklch(0 0 0 / 0.5) in every schemeOpacity doesn’t take part in contrast solving: a translucent color has no fixed perceived tone. Combining it with contrast emits a contrast and opacity warning:
glaze: color "<name>" has both "contrast" and "opacity".
Opacity makes perceived tone unpredictable.For an alpha that is solved against what it covers, use a shadow or glow or a transparent mix.
pastel toggles the hue-independent “safe” chroma limit for one color. It applies to every OKHSL ↔ sRGB conversion that touches the color: luminance during contrast solving, gamut clamping at sRGB blend and mix edges, and output formatting. The effective flag is carried on the resolved variant (ResolvedColorVariant.pastel), so formatting uses the same gamut mapping as 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({ format: 'rgb' }).light;
// → '--plain-color: rgb(105.64 102.82 227.87);\n' +
// '--soft-color: rgb(113.43 117.68 169.27);'Omit it to use the theme’s pastel (from its config override, default false). pastel can’t be set with glaze.configure().
The flag is part of the definition, so extend() copies it into child themes. Override it on the child to switch one 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- Shadows and glows:
pastelsets the gamut used to convert the pigment for the opacity solve and for output. The surface (bg) keeps its own flag. - Mixes:
pastelsets the gamut of the mixed result. The base and target keep their own flags, and the mix reads each in its gamut through the color it paints: a standard mix paints a pastel base or target exactly, and a pastel mix caps a vivid one at the pastel limit, keeping its hue and lightness. A transparent mix’s overlay is the target in that gamut. The contrast solver measures the color the mix paints. See Pastel mixes. - Color tokens: the same field exists on both
glaze.color()input forms and survives theexport()/colorFrom()round-trip; see Color tokens. splitHuerequires every emitted color to be pastel; see Output formats.
type Role = 'text' | 'surface' | 'border';
type RoleInput =
| Role
| 'bg' | 'background' | 'fill' | 'canvas' | 'paper' | 'layer'
| 'fg' | 'foreground' | 'content' | 'ink' | 'label' | 'stroke'
| 'divider' | 'outline' | 'separator' | 'hairline' | 'rule';A color’s role says how it is used against its base, and fixes APCA polarity: which of the two is measured as text. APCA is asymmetric (the Lc of a on b differs from b on a), so the role picks the argument order. WCAG is symmetric and ignores roles.
| Role | Polarity | Use | Aliases and name keywords |
|---|---|---|---|
'text' |
Foreground | Text, icons, foreground content | text, fg, foreground, content, ink, label, stroke |
'border' |
Foreground | Non-text spot elements: borders, dividers, outlines | border, divider, outline, separator, hairline, rule |
'surface' |
Background | Backgrounds and fills | surface, bg, background, fill, canvas, paper, layer |
A foreground color is measured as text over its base. A surface color is measured as the background, with its base as the text on top.
Each color’s role is the first of:
- An explicit
role, normalized from any alias above. - When
inferRoleis on (the default), a role inferred from the color’s name. The name is split on non-alphanumeric characters and camelCase humps, and the last recognized keyword wins:button-text→text,input-bg→surface,card-outline→border,inputBg→surface. - The opposite of the base’s role: a color on a
surfaceistext; a color ontextor aborderis asurface. 'text': the base is treated as the background.
Turn name inference off with glaze.configure({ inferRole: false }) or a per-theme config override; steps 1, 3, and 4 still apply. Shadows and glows always resolve as 'surface'. Mixes take the same role field and chain. Standalone glaze.color() tokens don’t: their role is an explicit role or 'text', never inferred from name (see Roles on tokens). The concept, with a polarity demo and an example, is in Contrast solving. The role helpers (normalizeRole, inferRoleFromName, roleToPolarity, oppositeRole) are in Role helpers.
theme.extend() copies every color into the child unless it has inherit: false:
const primary = glaze(280, 80);
primary.colors({
surface: { tone: 97 },
// stays on primary only
'brand-mark': { from: '#2f5bff', inherit: false },
});
const danger = primary.extend({ hue: 23 }); // has surface, not brand-markA child that inherits a color whose base was not inherited fails validation (references non-existent base). Redefine the base in the child’s colors, or mark the dependent inherit: false too. inherit works the same on shadows, glows, and mixes. See theme.extend().
Translucent overlays. Each emits a pigment (black for a shadow, white for a glow) with an opacity solved so that, once the browser composites it over bg, the surface lands exactly intensity tones darker (shadow) or lighter (glow). The two types share one shape; type picks the direction, which is fixed in every scheme.
| Field | Type | Default | Description |
|---|---|---|---|
type |
'shadow' | 'glow' |
— | Discriminator. A shadow always darkens and a glow always lightens, in light and dark. |
bg |
string |
— | The surface the overlay falls on. Required. Can’t be a shadow or glow. |
fg |
string |
— | The object casting the shadow or emitting the glow, such as the card. Scales the step: a darker object casts a deeper shadow, a lighter one a stronger glow. Omit for an exact shade or tint of intensity tones. Can’t be a shadow or glow. |
intensity |
HCPair<number> |
— | Tone step, 0–100: how many tones darker (shadow) or lighter (glow) than bg the composite lands. Clamped to 0–100. Required. |
tuning |
ShadowTuning / GlowTuning |
— | Per-color tuning, merged field by field over the global shadowTuning / glowTuning. A glow’s can set headroom; on a shadow’s, headroom throws. |
pastel |
boolean |
The theme’s pastel |
Gamut for the pigment. See Per-color pastel. |
inherit |
boolean |
true |
See inherit. |
const theme = glaze(280, 80);
theme.colors({
surface: { tone: 97 },
card: { tone: 100 },
accent: { tone: 45 },
'card-shadow': {
type: 'shadow',
bg: 'surface',
fg: 'card',
intensity: 8,
},
'surface-hover': { type: 'shadow', bg: 'surface', intensity: 5 },
'accent-pressed': { type: 'shadow', bg: 'accent', intensity: [8, 14] },
'accent-hover': { type: 'glow', bg: 'accent', intensity: 6 },
});
// 'card-shadow', light: oklch(0 0 0 / 0.1066)
// 'accent-hover', light: oklch(1 0 0 / 0.0948)- Object factor. With
fg, the step isintensity × k, wherek = max(0, 1 + β · d · (t_bg − t_fg) / 100),d = +1for a shadow and−1for a glow, andβistuning.objectInfluence. An object at the surface’s own tone gives exactlyintensity, as does omittingfg. - Per scheme.
bgandfgare read in each scheme, so the step andkadapt in dark and high contrast on their own. - HC pairs and the level.
intensityaccepts[normal, highContrast]and is interpolated by a manualcontrastLevel. - Output. Shadows and glows appear in every output method next to regular colors, with an alpha component. They have no
role(always'surface'), nomode, and no channels of their own;ResolvedColor.modeisundefinedfor them. - Unreachable steps. A glow on white or a deep shadow on near-black can’t reach the step. The result pins to the pigment’s strongest composite (full opacity, or transparent when the pigment can’t move the surface at all) and emits a deduped
console.warn. In high contrast and at a manualcontrastLevelother than0, where Glaze moves the surfaces itself, it warns only when the step is out of reach in both light and dark. So a shadow on anautolight surface pins quietly in dark high contrast: a tone-97 surface sits at tone 3 indarkContrast, socard-shadowandsurface-hoverabove pin to opaque black there, and light high contrast has room for them (accent-pressed, on a mid-tone accent, is unaffected). Use a glow for elevation in dark. A glow with aheadroomcan go past white instead; see HDR values. Details: When Glaze moves the surface. - Errors.
glaze: shadow "<name>" is missing "bg".,glaze: shadow "<name>" references non-existent bg "<ref>"., andglaze: glow "<name>" fg "<ref>" references a shadow color.(and the same for the other combinations).
How the opacity solve works, the object-factor table, state layers, and dark-mode elevation are in Shadows and glows. The standalone functions are in glaze.shadow() / glaze.glow().
interface LightingTuning {
objectInfluence?: number;
tint?: number;
reach?: number;
hueBleed?: number;
}
type ShadowTuning = LightingTuning;
interface GlowTuning extends LightingTuning {
headroom?: number;
}| Field | Default | Description |
|---|---|---|
objectInfluence |
0.5 |
β in the object factor. 0 removes fg’s effect on the step (k = 1); fg still feeds hueBleed. k tops out at 1 + β, for a black object on a white surface (shadow). |
tint |
0 |
Pigment saturation (0–1) in the surface’s hue. 0 is neutral black or white. Shows only with reach below 1, because pure black and white have no hue. On a neutral surface (saturation at most 1e-4, which covers every gray given as a value) the tint takes the object’s hue when hueBleed is above 0, and the pigment stays neutral otherwise. |
reach |
1 |
Where the pigment sits: the fraction (0–1) of the way from the surface’s tone to black (shadow) or white (glow). 1 is pure black or white, the most reach. Lower values let a tint show and cost reach. |
hueBleed |
0 |
Pull (0–1) of the pigment hue from the surface’s hue toward the object’s (fg) hue. Needs fg, a tint, and reach below 1 to show. |
headroom |
0 |
Glows only. Stops above SDR white a glow may use when its step crosses white: 1 allows up to 2× white in each linear channel, 2 up to 4×. A finite number ≥ 0; above 2 it clamps to 2 with a warning. On a shadow’s tuning it throws. See HDR values and Glow headroom. |
tint, reach, and hueBleed are clamped to 0–1. A field that is set must be a finite number, or the tuning throws (glaze: shadowTuning.reach must be a finite number (got NaN).); a field set to undefined counts as unset. Each field resolves separately, later wins:
- The defaults above.
- The global
shadowTuning(for shadows) orglowTuning(for glows), fromglaze.configure(). A theme or token config override that setsshadowTuning/glowTuningreplaces the global object as a whole. - The color’s
tuning.
theme.colors({
'shadow-tinted': {
type: 'shadow',
bg: 'surface',
intensity: 8,
tuning: { tint: 0.3, reach: 0.85 },
},
});
glaze.configure({
shadowTuning: { objectInfluence: 0.4 },
glowTuning: { objectInfluence: 0.6 },
});There is no opacity cap: when the step is reachable, it is exact. For a lighter overlay, lower intensity. What a tint costs on dark surfaces is shown in Tinted pigment.
With a headroom above 0, a glow whose target tone (the surface’s luminance tone + intensity × k) passes 100 while its SDR value is pinned at its brightest gets an HDR value on its resolved variant, next to the unchanged SDR one. What it is for and when it appears: HDR glows.
interface ResolvedHdr {
h: number; // base OKHSL hue, 0–360
s: number; // base OKHSL saturation, 0–1, never pastel
t: number; // base tone, 0–1
exposure: number; // gain over the base, in stops (> 0)
}
// ResolvedColorVariant.hdr?: ResolvedHdr- Base. What SDR paints at its brightest, taken opaque: the pigment at full opacity, or a white surface (tone 100) that no pigment can lift.
sis standard OKHSL saturation, even on apastelvariant.variant.tstays at most 1. - Exposure. The HDR color is the base’s linear sRGB times
2^exposure, withexposure = log2(min(Y(T*) / Y(base), 2^headroom / max channel))andT*the target tone: the exact step when the headroom allows it, otherwise capped so no linear channel passes2^headroom, with a warning. A step the HDR value covers doesn’t warn. - Absent elsewhere. The
hdrkey is missing (notundefined) on regular colors, shadows, mixes, glows that stay below white, glows whose pigment can’t lift a non-white surface at all, and every glow atheadroom: 0. The edge cases: When a glow gets an HDR value and Crossing white.
const theme = glaze(280, 80, { glowTuning: { headroom: 1 } });
theme.colors({
surface: { tone: 100 },
'surface-glow': { type: 'glow', bg: 'surface', intensity: 12 },
});
const glow = theme.resolve().get('surface-glow')!;
glaze.format(glow.light); // 'oklch(1 0 0 / 0)' — SDR can't lift white
glow.light.hdr?.exposure; // 0.5489…
glaze.formatHdr(glow.light);
// → 'color(srgb 1.1813 1.1813 1.1813)', tone 112
glow.dark.hdr; // undefined — the dark surface leaves room below whiteglaze.formatHdr() writes it as extended color(srgb R G B), and hdrToSrgb() / hdrToLinearSrgb() return its channels. Exporters emit it only with modes.hdr; see HDR output.
A blend of two colors: an opaque interpolation, or the target laid over the base with an alpha.
| Field | Type | Default | Description |
|---|---|---|---|
type |
'mix' |
— | Discriminator. |
base |
string |
— | The “from” color. Required. Can’t be a shadow or glow. |
target |
string |
— | The “to” color. Required. Can’t be a shadow or glow. |
value |
HCPair<number> |
— | Mix ratio 0–100: 0 is pure base, 100 pure target. With blend: 'transparent', the target’s opacity (value / 100). Clamped to 0–100. Required. |
blend |
'opaque' | 'transparent' |
'opaque' |
'opaque' produces a solid color. 'transparent' produces the target color with an alpha, for the browser to composite over the base. |
space |
'okhsl' | 'srgb' |
'okhsl' |
Interpolation space for 'opaque'. 'okhsl' interpolates hue (shortest arc), saturation, and lightness in OKHSL. 'srgb' interpolates linear-light sRGB channels, like CSS color-mix(in srgb-linear, …). Ignored for 'transparent'. |
contrast |
HCPair<ContrastSpec> |
— | Floor against base. The solver adjusts the ratio (opaque) or the opacity (transparent); an unreachable floor pins and warns. See Contrast and high contrast. |
pastel |
boolean |
The theme’s pastel |
Gamut of the mixed result. See Per-color pastel. |
role |
RoleInput |
Inferred | Role of the result against base, for APCA polarity. Same chain as on a regular color. |
inherit |
boolean |
true |
See inherit. |
theme.colors({
surface: { tone: 95 },
accent: { tone: 30 },
// black in every scheme
black: { tone: 0, saturation: 0, mode: 'static' },
'accent-tint': {
type: 'mix',
base: 'surface',
target: 'accent',
value: 30,
},
'surface-hover': {
type: 'mix',
base: 'surface',
target: 'black',
value: 8,
blend: 'transparent',
}, // the `black` color at alpha 0.08
'accent-wash': {
type: 'mix',
base: 'surface',
target: 'accent',
value: [10, 20],
contrast: 'AA',
},
});- Transparent output. The color is the target’s hue, saturation, and tone (in the mix’s gamut when their
pastelflags differ) withalpha = value / 100. The browser composites it withsource-overon gamma-encoded sRGB, and the contrast solve models exactly that, so a solved opacity renders at the contrast it was solved for. - Achromatic colors. In
'okhsl', a side with a saturation of zero or of float noise up to 1e-6 (a color defined withsaturation: 0, or a gray given as a value such as'#f5f5f5', which parses with zero saturation) has no hue, so the mix takes the other side’s hue, matching CSScolor-mix()“missing component” behavior. A white or black that keeps real saturation still carries its hue. A mix of two zero-saturation colors stays gray in either space. See Achromatic colors. - Chaining. A mix can use another mix as its
baseortarget. A transparent mix or anopacitycolor is accepted as a mix’sbase/targetor a shadow’s or glow’sbg/fg, but its alpha is ignored: its opaque color is used. autoFlip. Mixes have no per-colorautoFlip; the contrast solve uses the theme’s.- Adaptation. A mix reads its base and target in each scheme, so it adapts to dark and high contrast with them.
ResolvedColor.modeisundefinedfor mixes. - Errors.
glaze: mix "<name>" references non-existent base "<ref>".(ortarget), andglaze: mix "<name>" base "<ref>" references a shadow color.(orglow, ortarget).
Ramps of opaque and transparent mixes, contrast on mixes, and when to use a mix instead of a shadow, glow, or opacity are in Mixes and transparency.