@tenphi/cookbook 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/docs/customization-rules.md +7 -5
  2. package/docs/migration.md +52 -26
  3. package/docs/theme-and-components.md +32 -15
  4. package/docs/upstream/glaze/README.md +46 -26
  5. package/docs/upstream/glaze/docs/contrast.mdx +751 -0
  6. package/docs/upstream/glaze/docs/exporting.mdx +634 -0
  7. package/docs/upstream/glaze/docs/getting-started.mdx +489 -0
  8. package/docs/upstream/glaze/docs/hdr.mdx +523 -0
  9. package/docs/upstream/glaze/docs/methodology.mdx +922 -0
  10. package/docs/upstream/glaze/docs/migration.md +477 -286
  11. package/docs/upstream/glaze/docs/mixes.mdx +348 -0
  12. package/docs/upstream/glaze/docs/okhst.mdx +679 -0
  13. package/docs/upstream/glaze/docs/reference/color-definitions.md +552 -0
  14. package/docs/upstream/glaze/docs/reference/color-tokens.md +420 -0
  15. package/docs/upstream/glaze/docs/reference/configuration.md +527 -0
  16. package/docs/upstream/glaze/docs/reference/contrast.md +277 -0
  17. package/docs/upstream/glaze/docs/reference/index.md +225 -0
  18. package/docs/upstream/glaze/docs/reference/output.md +713 -0
  19. package/docs/upstream/glaze/docs/reference/palette.md +434 -0
  20. package/docs/upstream/glaze/docs/reference/theme.md +331 -0
  21. package/docs/upstream/glaze/docs/reference/utilities.md +646 -0
  22. package/docs/upstream/glaze/docs/schemes.mdx +635 -0
  23. package/docs/upstream/glaze/docs/shadows-and-glows.mdx +519 -0
  24. package/docs/upstream/manifest.json +2 -2
  25. package/package.json +3 -3
  26. package/docs/upstream/glaze/docs/api.md +0 -2021
  27. package/docs/upstream/glaze/docs/methodology.md +0 -686
  28. package/docs/upstream/glaze/docs/okhst.md +0 -207
@@ -1,2021 +0,0 @@
1
- # API Reference
2
-
3
- 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](methodology.md) for a guided walkthrough of how to use these primitives to build a real palette.
4
-
5
- ## Contents
6
-
7
- * [Theme creation](#theme-creation)
8
- * [Theme methods](#theme-methods)
9
- * [DTCG](#themedtcgoptions)
10
- * [DTCG Resolver-Module](#themedtcgresolveroptions)
11
- * [Tailwind CSS](#themetailwindoptions)
12
- * [High-contrast pairs](#high-contrast-pairs)
13
- * [Manual contrast level](#manual-contrast-level)
14
- * [Color definitions](#color-definitions)
15
- * [Dark seed](#dark-seed-darkhue--darksaturation)
16
- * [Standalone color tokens](#standalone-color-tokens)
17
- * [Shadows](#shadows)
18
- * [Mix colors](#mix-colors)
19
- * [Palette](#palette)
20
- * [Output formats](#output-formats)
21
- * [Hue channel splitting](#hue-channel-splitting-splithue)
22
- * [Adaptation modes](#adaptation-modes)
23
- * [Light / dark scheme mapping](#light--dark-scheme-mapping)
24
- * [Configuration](#configuration)
25
- * [Output modes](#output-modes)
26
- * [Validation](#validation)
27
- * [Color math utilities](#color-math-utilities)
28
-
29
- ***
30
-
31
- ## Theme creation
32
-
33
- | Method | Description |
34
- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
- | `glaze(hue, saturation?, config?)` | Create a theme from hue (0–360) and saturation (0–100). Optional `config` overrides the global config for this theme. |
36
- | `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](#dark-seed-darkhue--darksaturation). |
37
- | `glaze.themeFrom(data)` | Create a theme from a `theme.export()` snapshot (`kind: 'theme'`). |
38
- | `glaze.from(data)` | Compat alias for `glaze.themeFrom`. |
39
- | `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`). Extracts hue and saturation. |
40
- | `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255). Extracts hue and saturation. |
41
- | `glaze.paletteFrom(data)` | Create a palette from a `palette.export()` snapshot (`kind: 'palette'`). |
42
- | `glaze.colorFrom(data)` | Create a color token from a `token.export()` snapshot (`kind: 'color'`). |
43
- | `glaze.isThemeExport(data)` | Type guard for theme authoring snapshots. |
44
- | `glaze.isColorTokenExport(data)` | Type guard for color-token authoring snapshots. |
45
- | `glaze.isPaletteExport(data)` | Type guard for palette authoring snapshots. |
46
-
47
- ```ts
48
- const a = glaze(280, 80);
49
- const b = glaze({ hue: 280, saturation: 80 });
50
- const bDark = glaze({ hue: 280, saturation: 80, darkHue: 268, darkSaturation: 65 });
51
- const c = glaze.fromHex('#7a4dbf');
52
- const d = glaze.fromRgb(122, 77, 191);
53
- const e = glaze.themeFrom(a.export());
54
-
55
- // Per-theme config override:
56
- const rawTheme = glaze(280, 80, { lightTone: false, darkTone: false });
57
- ```
58
-
59
- Authoring restore triad (parallel to `.export()` on each instance):
60
-
61
- | Export | Restore |
62
- | ------------------ | --------------------- |
63
- | `theme.export()` | `glaze.themeFrom()` |
64
- | `token.export()` | `glaze.colorFrom()` |
65
- | `palette.export()` | `glaze.paletteFrom()` |
66
-
67
- 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.
68
-
69
- The optional `config` parameter is a `GlazeConfigOverride` — see [Per-instance config override](#per-instance-config-override).
70
-
71
- ***
72
-
73
- ## Theme methods
74
-
75
- A `GlazeTheme` exposes:
76
-
77
- | Method | Description |
78
- | --------------------------------- | --------------------------------------------------------------------------------------------------- |
79
- | `theme.hue` (readonly) | The hue seed (0–360). |
80
- | `theme.saturation` (readonly) | The saturation seed (0–100). |
81
- | `theme.darkHue` (readonly) | The dark-scheme hue seed (0–360), or `undefined` when `hue` is reused. |
82
- | `theme.darkSaturation` (readonly) | The dark-scheme saturation seed (0–100), or `undefined` when `saturation` is reused. |
83
- | `theme.colors(defs)` | Add/replace colors (additive merge — adds new, overwrites existing by name, doesn't remove others). |
84
- | `theme.color(name)` | Get a color definition by name. |
85
- | `theme.color(name, def)` | Set a single color definition. |
86
- | `theme.remove(name \| names[])` | Remove one or more color definitions. |
87
- | `theme.has(name)` | Check if a color is defined. |
88
- | `theme.list()` | List all defined color names. |
89
- | `theme.reset()` | Clear all color definitions. |
90
- | `theme.export(override?)` | Export the theme configuration as a JSON-safe object (optional config override at export time). |
91
- | `theme.extend(options)` | Create a child theme inheriting all color definitions (see [`extend`](#themeextendoptions) below). |
92
- | `theme.resolve()` | Resolve all colors and return a `Map<string, ResolvedColor>`. |
93
- | `theme.tokens(options?)` | Export as a flat token map grouped by scheme variant. |
94
- | `theme.tasty(options?)` | Export as [Tasty](../../tasty/README.md) style-to-state bindings. |
95
- | `theme.json(options?)` | Export as plain JSON. |
96
- | `theme.css(options?)` | Export as CSS custom property declarations. |
97
- | `theme.dtcg(options?)` | Export one W3C DTCG token tree per scheme. |
98
- | `theme.dtcgResolver(options?)` | Export one DTCG Resolver-Module document containing every scheme. |
99
- | `theme.tailwind(options?)` | Export a Tailwind CSS v4 theme and scheme overrides. |
100
-
101
- ### `theme.colors(defs)`
102
-
103
- ```ts
104
- theme.colors({ surface: { tone: 97 } });
105
- theme.colors({ text: { tone: 30 } });
106
- // Both 'surface' and 'text' are now defined.
107
- ```
108
-
109
- ### `theme.color(name) / theme.color(name, def)`
110
-
111
- ```ts
112
- theme.color('surface', { tone: 97, saturation: 0.75 }); // set
113
- const def = theme.color('surface'); // get
114
- ```
115
-
116
- ### `theme.extend(options)`
117
-
118
- Creates a new theme inheriting all color definitions, optionally replacing the hue / saturation seed, color overrides, and config:
119
-
120
- ```ts
121
- const danger = primary.extend({
122
- hue: 23,
123
- colors: { 'accent-fill': { tone: 48, mode: 'fixed' } },
124
- });
125
-
126
- // Inherit parent's config override and widen the dark window further:
127
- const highSat = base.extend({ config: { darkTone: [10, 100] } });
128
- ```
129
-
130
- `GlazeExtendOptions`:
131
-
132
- | Field | Type | Description |
133
- | ---------------- | --------------------- | -------------------------------------------------------------------------------------------- |
134
- | `hue` | `number` | Replace the hue seed. Defaults to the parent's hue. |
135
- | `saturation` | `number` | Replace the saturation seed. Defaults to the parent's saturation. |
136
- | `darkHue` | `number` | Replace the dark hue seed. Defaults to the parent's `darkHue`. |
137
- | `darkSaturation` | `number` | Replace the dark saturation seed. Defaults to the parent's `darkSaturation`. |
138
- | `colors` | `ColorMap` | Per-theme overrides (additive merge over the inherited map). |
139
- | `config` | `GlazeConfigOverride` | Config override for the child. Shallow-merged with the parent's override — child fields win. |
140
-
141
- Colors marked with `inherit: false` on the parent are **not** copied into the child.
142
-
143
- ### `theme.resolve()`
144
-
145
- Resolves the dependency graph and returns a
146
- `Map<string, ResolvedColor>`. Export methods call it automatically; use it
147
- directly for tests, diagnostics, or a custom output pipeline.
148
-
149
- ```ts
150
- const resolved = theme.resolve();
151
- const surface = resolved.get('surface');
152
- // surface?.light.t is canonical tone on 0–1.
153
- ```
154
-
155
- ### `theme.tokens(options?)`
156
-
157
- Flat token map grouped by scheme variant.
158
-
159
- ```ts
160
- theme.tokens();
161
- // → { light: { surface: 'oklch(...)' }, dark: { surface: 'oklch(...)' } }
162
- ```
163
-
164
- `GlazeJsonOptions`:
165
-
166
- | Option | Default | Description |
167
- | -------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
168
- | `format` | `'oklch'` | Output color format. One of `'rgb' \| 'hsl' \| 'oklch'`. `'okhsl'` and `'okhst'` throw — use `tasty()` for those. |
169
- | `modes` | `{ dark: true, highContrast: false }` (or global config) | Which scheme variants to include. |
170
-
171
- ### `theme.tasty(options?)`
172
-
173
- Style-to-state bindings for the [Tasty](../../tasty/README.md) 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.
174
-
175
- ```ts
176
- theme.tasty();
177
- // → {
178
- // '#surface': { '': 'oklch(...)', '@media(prefers-color-scheme: dark)': 'oklch(...)' },
179
- // ...
180
- // }
181
- ```
182
-
183
- `GlazeTokenOptions`:
184
-
185
- | Option | Default | Description |
186
- | --------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
187
- | `format` | `'oklch'` | Output color format. `'okhsl'` and `'okhst'` are also supported here ([Tasty](../../tasty/README.md)-only spaces). |
188
- | `modes` | global config | Which scheme variants to include. |
189
- | `states.dark` | `'@media(prefers-color-scheme: dark)'` (or global config) | State alias for dark mode tokens. |
190
- | `states.highContrast` | `'@media(prefers-contrast: more)'` (or global config) | State alias for high-contrast tokens. |
191
- | `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. |
192
- | `name` | `'theme'` | Base name for the theme-level hue var (`$theme-hue` / `--theme-hue`). Palette export auto-derives this from the theme name. |
193
- | `prefix` | (palette only) | See [Palette](#palette). |
194
-
195
- 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)'`).
196
-
197
- ### `theme.json(options?)`
198
-
199
- Per-color JSON map.
200
-
201
- ```ts
202
- theme.json();
203
- // → {
204
- // surface: { light: 'oklch(...)', dark: 'oklch(...)' },
205
- // text: { light: 'oklch(...)', dark: 'oklch(...)' },
206
- // }
207
- ```
208
-
209
- Same options as `tokens()`.
210
-
211
- ### `theme.css(options?)`
212
-
213
- CSS custom property declaration strings, grouped by scheme variant.
214
-
215
- ```ts
216
- theme.css();
217
- // → {
218
- // light: '--surface-color: oklch(...);\n--text-color: oklch(...);',
219
- // dark: '--surface-color: oklch(...);\n--text-color: oklch(...);',
220
- // lightContrast: '...',
221
- // darkContrast: '...',
222
- // }
223
- ```
224
-
225
- `GlazeCssOptions`:
226
-
227
- | Option | Default | Description |
228
- | ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
229
- | `format` | `'oklch'` | Output color format. One of `'rgb' \| 'hsl' \| 'oklch'`. `'okhsl'` and `'okhst'` throw — use `tasty()` for those. |
230
- | `suffix` | `'-color'` | Suffix appended to each CSS property name. Pass `''` for bare property names. |
231
- | `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). |
232
- | `name` | `'theme'` | Base name for the theme-level hue var (`--theme-hue`). Palette export auto-derives this from the theme name. |
233
-
234
- `GlazeCssResult` always contains all four keys (`light`, `dark`, `lightContrast`, `darkContrast`); empty if no colors are defined for that variant.
235
-
236
- ### `theme.dtcg(options?)`
237
-
238
- W3C [Design Tokens Format Module (2025.10)](https://www.designtokens.org/) 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.
239
-
240
- ```ts
241
- theme.dtcg();
242
- // → {
243
- // light: {
244
- // surface: {
245
- // $type: 'color',
246
- // $value: { colorSpace: 'srgb', components: [0.96, 0.94, 0.98], hex: '#f5f0fa' },
247
- // },
248
- // },
249
- // dark: {
250
- // surface: {
251
- // $type: 'color',
252
- // $value: { colorSpace: 'srgb', components: [0.16, 0.14, 0.2], hex: '#292333' },
253
- // },
254
- // },
255
- // }
256
- ```
257
-
258
- 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).
259
-
260
- `GlazeDtcgOptions`:
261
-
262
- | Option | Default | Description |
263
- | ------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
264
- | `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. |
265
- | `modes` | global config | Which scheme variants to include. `light` is always present. |
266
-
267
- `alpha` is included on `$value` only when the color's opacity is below 1. `$type` is always `'color'`.
268
-
269
- ### `theme.dtcgResolver(options?)`
270
-
271
- A single W3C [DTCG Resolver-Module](https://www.designtokens.org/) 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.
272
-
273
- ```ts
274
- theme.dtcgResolver({ modes: { highContrast: true } });
275
- // → {
276
- // version: '2025.10',
277
- // sets: {
278
- // base: {
279
- // sources: [
280
- // {
281
- // surface: {
282
- // $type: 'color',
283
- // $value: { colorSpace: 'srgb', components: [0.96, 0.94, 0.98], hex: '#f5f0fa' },
284
- // },
285
- // },
286
- // ],
287
- // },
288
- // },
289
- // modifiers: {
290
- // scheme: {
291
- // default: 'light',
292
- // contexts: {
293
- // light: [],
294
- // dark: [
295
- // {
296
- // surface: {
297
- // $type: 'color',
298
- // $value: { colorSpace: 'srgb', components: [0.16, 0.14, 0.2], hex: '#292333' },
299
- // },
300
- // },
301
- // ],
302
- // lightContrast: [ /* … */ ],
303
- // darkContrast: [ /* … */ ],
304
- // },
305
- // },
306
- // },
307
- // resolutionOrder: [
308
- // { $ref: '#/sets/base' },
309
- // { $ref: '#/modifiers/scheme' },
310
- // ],
311
- // }
312
- ```
313
-
314
- **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.
315
-
316
- `GlazeDtcgResolverOptions` (extends `GlazeDtcgOptions`, so `modes` and `colorSpace` pass through):
317
-
318
- | Option | Default | Description |
319
- | -------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- |
320
- | `colorSpace` | `'srgb'` | Same as `dtcg()` — flows through to every source and context. |
321
- | `modes` | global config | Which scheme variants to emit as contexts. `light` is always present (the default); absent variants are omitted. |
322
- | `setName` | `'base'` | Name of the single set holding the default (light) token tree. |
323
- | `modifierName` | `'scheme'` | Name of the modifier describing the scheme axis. |
324
- | `contextNames` | identity | Override the four context names (`light` / `dark` / `lightContrast` / `darkContrast`) — e.g. `{ dark: 'night' }`. |
325
- | `version` | `'2025.10'` | Resolver document version. |
326
-
327
- ### `theme.tailwind(options?)`
328
-
329
- 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.
330
-
331
- ```css
332
- @theme {
333
- --color-surface: oklch(0.96 0.01 280);
334
- --color-text: oklch(0.3 0.05 280);
335
- }
336
- .dark {
337
- --color-surface: oklch(0.16 0.01 280);
338
- --color-text: oklch(0.85 0.05 280);
339
- }
340
- .high-contrast {
341
- --color-surface: oklch(0.98 0.01 280);
342
- --color-text: oklch(0.1 0.05 280);
343
- }
344
- .dark.high-contrast {
345
- --color-surface: oklch(0.05 0.01 280);
346
- --color-text: oklch(0.95 0.05 280);
347
- }
348
- ```
349
-
350
- `GlazeTailwindOptions`:
351
-
352
- | Option | Default | Description |
353
- | ---------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
354
- | `format` | `'oklch'` | Output color format for the values. |
355
- | `namespace` | `'color-'` | CSS custom property namespace, forming `--<namespace><name>` (e.g. `--color-surface`). Named `namespace` to avoid clashing with the palette theme-prefix option. |
356
- | `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). |
357
- | `highContrastSelector` | `'.high-contrast'` | Selector wrapping the light high-contrast overrides. The combined dark + high-contrast block uses `${darkSelector}${highContrastSelector}` (e.g. `.dark.high-contrast`). |
358
- | `modes` | global config | Which scheme variants to include. The `@theme` block (light) is always emitted when colors exist. |
359
-
360
- ### `theme.export(override?)`
361
-
362
- ```ts
363
- const snapshot = theme.export();
364
- // → {
365
- // kind: 'theme',
366
- // version: 1,
367
- // hue: 280,
368
- // saturation: 80,
369
- // colors: { surface: { ... }, ... },
370
- // config: { lightTone: {...}, darkTone: {...}, pastel: false, ... },
371
- // }
372
-
373
- const restored = glaze.themeFrom(snapshot);
374
- ```
375
-
376
- Returns a deep-cloned, JSON-safe authoring snapshot (definitions + frozen
377
- effective config — not resolved color strings). Freezes
378
- `getConfig() ∪ instance local ∪ override` at call time. Restored themes pin
379
- that freeze as their local override. Distinct from `theme.json()`, which
380
- emits resolved color strings.
381
-
382
- ***
383
-
384
- ## High-contrast pairs
385
-
386
- `HCPair<T>` means either one value used in both ordinary and high-contrast
387
- schemes, or an explicit `[normal, highContrast]` pair:
388
-
389
- ```ts
390
- type HCPair<T> = T | [T, T];
391
- ```
392
-
393
- It is used by `tone`, `contrast`, shadow `intensity`, and mix `value`:
394
-
395
- ```ts
396
- tone: '-8'; // -8 in normal and HC
397
- tone: ['-8', '-16']; // -8 normal, -16 HC
398
- contrast: {
399
- apca: 'content';
400
- } // preset with automatic HC enhancement
401
- contrast: {
402
- apca: ['content', 'body'];
403
- } // explicit normal/HC targets
404
- ```
405
-
406
- For `contrast`, the pair may wrap the whole spec or live inside the selected
407
- metric. An explicit HC value disables automatic APCA enhancement or WCAG preset
408
- promotion for that color.
409
-
410
- ***
411
-
412
- ## Manual contrast level
413
-
414
- `contrastLevel` puts the normal end of the two-tier contrast model on a `0–100`
415
- slider. It is a config field, so it works globally, per theme, per token, and
416
- through `extend()`:
417
-
418
- ```ts
419
- glaze.configure({ contrastLevel: 60 });
420
- const theme = glaze(280, 80, { contrastLevel: 60 });
421
- glaze.color('#26fcb2', { contrastLevel: 60 });
422
- ```
423
-
424
- The level moves the **normal** variants — `light` and `dark` — along that slider.
425
- It says nothing about high contrast: `lightContrast` / `darkContrast` stay the
426
- true high-contrast resolution at every level, and
427
- [`modes.highContrast`](#output-modes) alone decides whether they are emitted.
428
-
429
- | Value | Meaning |
430
- | ---------------- | -------------------------------------------------------------------------- |
431
- | `'auto'` | Default. The normal variants take the authored normal entries as-is. |
432
- | `0` | The same thing, reached through the slider — output-identical to `'auto'`. |
433
- | `100` | The normal variants *are* the high-contrast ones, so the tier is dropped. |
434
- | anything between | The normal variants are resolved *at* that level. |
435
-
436
- Levels `0` and `100` reproduce the classic `light` / `dark` and
437
- `lightContrast` / `darkContrast` output **bit for bit**.
438
-
439
- ### What the level interpolates
440
-
441
- Exactly the three things that make a high-contrast variant differ from its
442
- normal counterpart:
443
-
444
- | Mechanism | At level `L` |
445
- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
446
- | Authored `HCPair`s — `tone`, `contrast`, shadow `intensity`, mix `value` | the two ends are interpolated |
447
- | The tone-window bypass | each endpoint moves toward the full range: light `10→0`, dark `15→0 / 95→100` |
448
- | Contrast escalation — `AA → AAA`, APCA `+15 Lc` | the two numeric targets are interpolated (`AA` at level 50 solves for 5.75) |
449
-
450
- The interpolated values are then fed through the ordinary resolve, so a
451
- contrast floor at any level is **solved**, not approximated, and `autoFlip`
452
- decides once per level.
453
-
454
- These feed the two **normal** passes only. The high-contrast passes bypass the
455
- level entirely, so the tier resolves exactly as it does in `'auto'` mode.
456
-
457
- Everything that does not vary by high contrast today — hue, saturation,
458
- `darkDesaturation`, `opacity`, `pastel` — is unaffected by the level.
459
-
460
- ### Un-interpolable tone pairs switch at 50
461
-
462
- Two ends of different kinds have no midpoint, because blending them would
463
- change *which* resolution rule applies partway up the ramp. These switch from
464
- the normal entry to the high-contrast one at level 50:
465
-
466
- ```ts
467
- tone: [50, 'max']; // absolute vs. extreme
468
- tone: [50, '+20']; // absolute vs. relative
469
- tone: ['max', 'min']; // opposite extremes
470
- ```
471
-
472
- Same-kind pairs (`[30, 20]`, `['-6', '-12']`, `{ wcag: [4.5, 7] }`,
473
- `[{ apca: 60 }, { apca: 90 }]`) interpolate smoothly.
474
-
475
- A `contrast` pair may **not** switch metric — `[4.5, { apca: 75 }]` throws. A
476
- WCAG ratio and an APCA Lc are different scales, so no target exists between
477
- them, and even in `'auto'` mode the two variants would be incomparable. Pair
478
- values inside one metric instead: `{ wcag: [4.5, 7] }` or `{ apca: [60, 90] }`.
479
-
480
- ### A color never swaps sides mid-slider
481
-
482
- [`autoFlip`](#autoflip) decides per solve, and its tie-break — when both
483
- directions meet the floor, take the one nearer the authored tone — depends on
484
- the target. Along a ramp of targets that criterion shifts, so a naive
485
- implementation lets a color leap across its own base as the slider moves.
486
-
487
- Under a manual level the side is instead decided once, from the nearer
488
- endpoint's target, and preferred at every level in that half of the ramp.
489
- Consequences:
490
-
491
- * A color whose two ends land on the same side of its base **never** changes
492
- side, at any level.
493
- * A color whose ends genuinely disagree — no single side can satisfy both its
494
- normal and its high-contrast floor — changes side exactly once, at level 50,
495
- the same place un-interpolable tone pairs switch.
496
- * Flipping is not disabled, only re-ordered, so a side that physically cannot
497
- reach the requested contrast still falls back to the opposite one. The floor
498
- is met at every level.
499
-
500
- The underlying solver option is `preferInitial` on
501
- [`findToneForContrast`](#contrast-solver), should you need the same behavior
502
- directly.
503
-
504
- ### High-contrast output
505
-
506
- The level and the high-contrast tier are independent, and they compose: the
507
- slider raises the baseline, and a `prefers-contrast: more` block still escalates
508
- on top of whatever baseline the user has chosen.
509
-
510
- * `resolve()` returns four variants at every level. `lightContrast` /
511
- `darkContrast` are the **true** high-contrast resolution — bit-identical to
512
- what `'auto'` resolves for those slots, at any level.
513
- * [`modes.highContrast`](#output-modes) alone decides whether the tier is
514
- emitted, exactly as in `'auto'` mode. A level does not turn it off.
515
- * `css()` always returns four strings and ignores `modes`, so its
516
- `lightContrast` / `darkContrast` strings now carry genuinely escalated values
517
- at a mid level, where they previously repeated the normal declarations. An
518
- existing `@media (prefers-contrast: more)` block starts doing real work.
519
- * A level on a **single theme or token** does not change which modes are
520
- emitted — `modes` is global-only, and a palette must not have one sibling
521
- collapse the shared token structure.
522
-
523
- | `contrastLevel` | `modes.highContrast: false` (default) | `modes.highContrast: true` |
524
- | --------------- | ------------------------------------- | ----------------------------------------- |
525
- | `'auto'` / `0` | `light`, `dark` | + `lightContrast`, `darkContrast` |
526
- | `1`–`99` | `light`, `dark` at the level | + the tier, unchanged by the level |
527
- | `100` (global) | `light`, `dark` = the HC values | same — the tier is dropped as a duplicate |
528
-
529
- That last row is the one exception. At level 100 the normal variants *are* the
530
- high-contrast ones, so a separate tier would be an exact duplicate: a global
531
- level of 100 emits a single light/dark set, and even an explicit
532
- `modes: { highContrast: true }` on the call is suppressed, because it has nothing
533
- different left to ask for. A level of 100 on a single theme or token still
534
- reports into the tier, with values equal to its own normal ones.
535
-
536
- ### Driving it at runtime
537
-
538
- `configure()` bumps the config version, which invalidates every theme and token
539
- cache, so a slider only needs to re-export:
540
-
541
- ```ts
542
- slider.oninput = () => {
543
- glaze.configure({ contrastLevel: slider.valueAsNumber });
544
- apply(theme.css());
545
- };
546
- ```
547
-
548
- A manual resolve runs the same four passes as an `'auto'` one. Only level 100
549
- skips the two high-contrast passes, because the normal ones already produced
550
- those values.
551
-
552
- ### Clearing it, exporting it, base links
553
-
554
- * **`configure()` never clears a field by omission.** Pass `'auto'` explicitly
555
- to leave manual mode — globally, or on one theme of a palette.
556
- * **`.export()` freezes only an *authored* level.** A level set on the instance
557
- (or passed to `.export()`) is authored intent and is written to the snapshot;
558
- a level merely inherited from the global config is a live user preference and
559
- is left out, so a restored theme still follows the current slider. This
560
- matches how `modes` and `states` are treated.
561
- * **Set the level globally, or on both sides of a base link.** A per-token level
562
- on a dependent but not on its base anchors the two at different levels — the
563
- same caveat as a per-instance `lightTone` override. It is visible in the
564
- high-contrast tier too, since a dependent's high-contrast variant reads its
565
- base's high-contrast slots (and at level 100, the base's mirrored normal ones).
566
-
567
- `resolveContrastForLevel(spec, level, polarity?)` is exported for advanced use;
568
- see [Contrast solver](#contrast-solver).
569
-
570
- ***
571
-
572
- ## Color definitions
573
-
574
- `ColorDef` is a discriminated union:
575
-
576
- ```ts
577
- type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
578
- ```
579
-
580
- ### `RegularColorDef`
581
-
582
- | Field | Type | Description |
583
- | ---------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
584
- | `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`](#from-a-literal-color). |
585
- | `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`. |
586
- | `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. |
587
- | `hue` | `number \| RelativeValue` | Number = absolute (0–360). String (`'+N'`/`'-N'`) = relative to the **theme seed hue** (never to a base color). |
588
- | `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](#dark-seed-darkhue--darksaturation). |
589
- | `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. |
590
- | `base` | `string` | Name of another color in the same theme — makes this a *dependent* color. |
591
- | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base`. Requires `base`. See [`contrast`](#contrast-floor). |
592
- | `mode` | `'auto' \| 'fixed' \| 'static'` | Adaptation mode. Default: `'auto'`. See [Adaptation modes](#adaptation-modes). |
593
- | `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`](#autoflip). |
594
- | `opacity` | `number` | Fixed alpha 0–1. Output includes alpha in the CSS value. Combining with `contrast` is not recommended (a `console.warn` is emitted). |
595
- | `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`](#per-color-pastel). |
596
- | `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](#roles). |
597
- | `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. |
598
-
599
- #### `from` (a literal color)
600
-
601
- Most of Glaze answers the question "design me a palette". `from` answers the
602
- other one: **"honor this color."** White-label products, multi-tenant branding
603
- and imported design tokens all arrive with a value already chosen, and it is a
604
- contract rather than a starting point.
605
-
606
- ```ts
607
- const theme = glaze(280, 80);
608
-
609
- theme.colors({
610
- surface: { tone: 100, saturation: 0.12 },
611
- brand: { from: '#2f5bff', base: 'surface', contrast: 3 },
612
- });
613
- ```
614
-
615
- It accepts the same values as [`glaze.color()`](#input-forms) and supplies three
616
- things at once: `hue`, `tone`, and — uniquely among theme colors — an
617
- **absolute saturation**.
618
-
619
- That last one is the point. Every other color's `saturation` is a 0–1 factor of
620
- the theme seed, so the seed is a ceiling: without `from`, the only way to place
621
- a color more saturated than its theme is to re-seed the whole theme, dragging
622
- every sibling along. A `from` color carries its own chroma and is unaffected by
623
- the seed:
624
-
625
- ```ts
626
- // Identical output at every seed saturation — the color is the color.
627
- for (const seed of [5, 50, 100]) {
628
- glaze(280, seed).colors({ brand: { from: '#2f5bff' } }); // → #2f5bff
629
- }
630
- ```
631
-
632
- **What "exactly" covers.** The **light, normal-contrast** variant reproduces the
633
- value (a local `lightTone: false`, the same default the value-shorthand form of
634
- `glaze.color()` applies). Dark and high contrast adapt as usual. That asymmetry
635
- is deliberate: those are the variants a reader reaches for when the normal one
636
- does not work for them, so readability outranks fidelity there — and a color
637
- pinned across all four would just be a worse `mode: 'static'`.
638
-
639
- A `contrast` floor still applies in every scheme and is still a floor, not a
640
- target: a value that already clears it is emitted untouched, and one that misses
641
- moves only as far as the floor. Because the floor is solved per scheme, which
642
- scheme comes out exact depends on the color — a light brand cannot clear 3:1 on
643
- a white page but clears it easily on a dark one.
644
-
645
- Sibling fields win over what the value supplied, so `{ from: '#2f5bff', hue: 300 }`
646
- keeps the saturation and tone and rotates the hue. Note that a `saturation`
647
- written alongside `from` reverts to its usual meaning — a factor of the seed,
648
- not of the color.
649
-
650
- A `from` color needs no `base` and no `tone`: it is placed absolutely, so it
651
- stands as a root on its own. Add `base` + `contrast` when it has to stay legible
652
- against something.
653
-
654
- #### Tone values
655
-
656
- `tone` (0–100) replaces authored OKHSL lightness with a contrast-shaped axis.
657
- Equal tone differences give equal WCAG contrast for neutrals; chromatic results
658
- can drift in measured luminance. See [OKHST in Glaze](okhst.md). To port old
659
- `lightness` values, see [migration.md](migration.md).
660
-
661
- | Form | Example | Meaning |
662
- | ------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
663
- | Number (absolute) | `tone: 45` | Absolute tone 0–100. |
664
- | String (tone delta) | `tone: '-52'` | Signed difference from the base color's resolved tone (requires `base`). |
665
- | 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. |
666
- | HC pair | `tone: ['-7', '-20']` | `[normal, high-contrast]`. A single value applies to both. |
667
-
668
- **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.
669
-
670
- **A tone delta** applies a signed difference to the base color's resolved tone.
671
- It gives an exact contrast step for neutrals and a stable visual progression for
672
- chromatic colors. In dark mode with `mode: 'auto'`, it is anchored to the
673
- base's per-scheme tone. If `base + delta` falls outside `[0, 100]`, the result
674
- is clamped to the boundary, or — with `autoFlip` (default on) — mirrored to the
675
- other side of the base. If the mirrored target is also out of range, the original
676
- delta is kept and clamped on the authored side.
677
-
678
- **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.
679
-
680
- **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.
681
-
682
- ```ts
683
- theme.colors({
684
- bg: { tone: 60 },
685
- fg: { base: 'bg', tone: 'max' },
686
- });
687
- // light: bg 62.0, fg 100.0 — shift +38.0, contrast 3.18:1
688
- // dark: bg 43.9, fg 5.9 — shift -38.0, contrast 3.18:1
689
- ```
690
-
691
- A dependent color with `base` but no `tone` inherits the base's tone (equivalent to a delta of 0).
692
-
693
- #### `autoFlip`
694
-
695
- `autoFlip` governs what happens when a result would fall outside its valid range:
696
-
697
- * **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.
698
- * **`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`).
699
-
700
- `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.
701
-
702
- #### `contrast` (floor)
703
-
704
- ```ts
705
- type ContrastPreset = 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
706
- type ContrastSpec =
707
- | number // bare WCAG ratio
708
- | ContrastPreset // named WCAG preset
709
- | { wcag: HCPair<number | ContrastPreset> }
710
- | { apca: HCPair<number | ApcaPreset> }; // APCA Lc target
711
- ```
712
-
713
- | Preset | WCAG ratio |
714
- | ------------- | ---------- |
715
- | `'AA-large'` | 3 |
716
- | `'AA'` | 4.5 |
717
- | `'AAA-large'` | 4.5 |
718
- | `'AAA'` | 7 |
719
-
720
- 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] }`).
721
-
722
- ```ts
723
- contrast: 4.5; // WCAG 4.5
724
- contrast: 'AAA'; // WCAG 7
725
- contrast: {
726
- wcag: 6;
727
- } // WCAG 6
728
- contrast: {
729
- wcag: [4.5, 7];
730
- } // WCAG 4.5 normal / 7 high-contrast (explicit)
731
- contrast: {
732
- apca: 60;
733
- } // APCA Lc 60 normal / 75 high-contrast (auto)
734
- contrast: {
735
- apca: [45, 60];
736
- } // APCA Lc 45 normal / 60 high-contrast (explicit)
737
- contrast: {
738
- apca: 'content';
739
- } // APCA preset -> Lc 60 normal / 75 high-contrast (auto)
740
- contrast: {
741
- apca: ['content', 'body'];
742
- } // Lc 60 normal / 75 high-contrast (explicit)
743
- ```
744
-
745
- **WCAG HC auto-promotion:** a bare WCAG preset (no `[normal, hc]` pair at either
746
- the outer `contrast` or inner `wcag` level) is automatically promoted to its
747
- spec-defined "Enhanced" successor in high-contrast mode — `AA` → `AAA` (4.5 → 7)
748
- and `AA-large` → `AAA-large` (3 → 4.5), per WCAG SC 1.4.3 → 1.4.6. `AAA` and
749
- `AAA-large` are already the top WCAG tier and are left unchanged; bare numeric
750
- targets have no successor tier and are also left unchanged. An explicit HC value
751
- via either pair overrides and skips the promotion.
752
-
753
- **APCA Enhanced Level (HC auto-boost):** a bare APCA scalar (no `[normal, hc]`
754
- pair at either the outer `contrast` or inner `apca` level) is automatically
755
- boosted by **+15 Lc** in high-contrast mode, the APCA analog of WCAG's
756
- AAA-over-AA step. On by default; an explicit HC value via either pair
757
- overrides it and skips the boost. The enhanced target is clamped to 106 Lc.
758
- For large/bold text (where APCA caps contrast at Lc 90 to avoid glare), pass
759
- an explicit HC pair to hold that ceiling.
760
-
761
- APCA preset keywords (Bronze Simple Mode conformance levels, role-independent):
762
- `'preferred'` (Lc 90), `'body'` (75), `'content'` (60, \~AA), `'large'` (45, \~3:1),
763
- `'non-text'` (30), `'min'` (15, point of invisibility).
764
-
765
- The floor is applied independently per scheme. If the preferred `tone` already
766
- satisfies it, the tone is kept; otherwise the solver uses the tone-shaped scale
767
- for a closed-form WCAG seed and fast search until the target is met.
768
-
769
- 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`](#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.
770
-
771
- **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.
772
-
773
- **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](okhst.md#contrast-verification).
774
-
775
- #### Per-color hue override
776
-
777
- ```ts
778
- const theme = glaze(280, 80);
779
- theme.colors({
780
- surface: { tone: 97 },
781
- gradientEnd: { tone: 90, hue: '+20' }, // 280 + 20 = 300
782
- warning: { tone: 60, hue: 40 }, // absolute
783
- });
784
- ```
785
-
786
- Relative hue is always relative to the **theme seed hue**, not to a base color.
787
-
788
- #### Dark seed (`darkHue` / `darkSaturation`)
789
-
790
- Tone inverts automatically between light and dark, but hue and saturation do
791
- not — by default every scheme reuses the one seed, with dark getting a flat
792
- `darkDesaturation` haircut. When a palette needs a genuinely different chroma in
793
- dark (a cooler accent, a calmer surface tint), author a second seed:
794
-
795
- ```ts
796
- const theme = glaze({
797
- hue: 280,
798
- saturation: 80,
799
- darkHue: 268, // dark schemes seed from 268 instead of 280
800
- darkSaturation: 65, // and from 65 instead of 80
801
- });
802
- ```
803
-
804
- Individual colors can override either channel for dark on their own:
805
-
806
- ```ts
807
- theme.colors({
808
- surface: { tone: 97 },
809
- accent: { tone: 55, hue: '+20', darkHue: '+35' },
810
- warning: { tone: 60, saturation: 0.9, darkSaturation: 0.6 },
811
- });
812
- ```
813
-
814
- Both apply to the `dark` **and** `darkContrast` variants. The rules:
815
-
816
- | Rule | Behavior |
817
- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
818
- | 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). |
819
- | Fallback | `darkHue` falls back to `hue`, `darkSaturation` to `saturation`, and the def falls back to the seed. Omitting everything reproduces the previous behavior exactly. |
820
- | 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. |
821
- | Absolute hue | An absolute `hue` (no `darkHue`) is used verbatim in dark — absolute means absolute in every scheme. |
822
- | `darkDesaturation` | Bypassed as soon as any explicit dark saturation is authored, at either level. The value you write is the value you get. |
823
- | `mode: 'static'` | Ignores both. A static color is pinned to one hue and saturation across every scheme. |
824
- | 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. |
825
-
826
- With [`splitHue`](#hue-channel-splitting-splithue), a dark seed makes the hue
827
- custom properties scheme-dependent; Glaze re-declares them in the dark block
828
- automatically.
829
-
830
- #### Per-color `pastel`
831
-
832
- `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.
833
-
834
- ```ts
835
- const theme = glaze(280, 80);
836
- theme.colors({
837
- plain: { tone: 50, saturation: 1 },
838
- soft: { tone: 50, saturation: 1, pastel: true },
839
- });
840
- // theme.resolve().get('soft')!.light.pastel === true
841
- // theme.css().light contains different rgb() triples for `--plain` and `--soft`
842
- ```
843
-
844
- 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.
845
-
846
- 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:
847
-
848
- ```ts
849
- const parent = glaze(280, 80);
850
- parent.colors({ soft: { tone: 50, saturation: 1, pastel: true } });
851
-
852
- const child = parent.extend({
853
- colors: { soft: { tone: 50, saturation: 1, pastel: false } },
854
- });
855
- // child.resolve().get('soft')!.light.pastel === false
856
- ```
857
-
858
- > **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.
859
- >
860
- > 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.
861
-
862
- #### Roles
863
-
864
- 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.
865
-
866
- | Role | Polarity | Use | Aliases (name inference) |
867
- | ----------- | -------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
868
- | `'text'` | fg | Text / icons / foreground content | `text`, `fg`, `foreground`, `content`, `ink`, `label`, `stroke` |
869
- | `'border'` | fg | Non-text spot elements (borders, dividers, outlines) | `border`, `divider`, `outline`, `separator`, `hairline`, `rule` |
870
- | `'surface'` | bg | Backgrounds / fills | `surface`, `bg`, `background`, `fill`, `canvas`, `paper`, `layer` |
871
-
872
- Resolution chain (per color):
873
-
874
- 1. Explicit `role` (normalized from an alias) wins.
875
- 2. Else, when `inferRole` is enabled (default), infer from the color name — the **last** recognized token wins (`button-text` → `text`, `input-bg` → `surface`, `card-outline` → `border`).
876
- 3. Else, the opposite of the base's role (a `surface` base ⇒ this is `text`).
877
- 4. Else, `'text'` (foreground) — i.e. the base is treated as the background.
878
-
879
- ```ts
880
- const theme = glaze(280, 60);
881
- theme.colors({
882
- surface: { tone: 90 },
883
- text: { base: 'surface', contrast: { apca: 'content' } }, // inferred text
884
- border: { base: 'surface', tone: '-10' }, // inferred border
885
- });
886
- // role fixes APCA polarity; set `pastel: true` explicitly if a border
887
- // needs the hue-independent safe chroma limit.
888
- ```
889
-
890
- Disable name inference with `glaze.configure({ inferRole: false })` (the base-opposite and foreground-default fallbacks still apply).
891
-
892
- ### `ShadowColorDef`
893
-
894
- | Field | Type | Description |
895
- | ----------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
896
- | `type` | `'shadow'` | Discriminator. |
897
- | `bg` | `string` | Background color name — must reference a non-shadow color in the same theme. |
898
- | `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. |
899
- | `intensity` | `HCPair<number>` | Shadow intensity, 0–100. Supports HC pairs. |
900
- | `tuning` | `ShadowTuning` | Per-color tuning overrides. Merged field-by-field with the global `shadowTuning`. |
901
- | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
902
- | `inherit` | `boolean` | Inheritance flag, default `true`. |
903
-
904
- See [Shadows](#shadows) below for the algorithm and tuning details.
905
-
906
- ### `MixColorDef`
907
-
908
- | Field | Type | Description |
909
- | ---------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
910
- | `type` | `'mix'` | Discriminator. |
911
- | `base` | `string` | "From" color name. |
912
- | `target` | `string` | "To" color name. |
913
- | `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. |
914
- | `blend` | `'opaque' \| 'transparent'` | Default `'opaque'`. |
915
- | `space` | `'okhsl' \| 'srgb'` | Interpolation space for opaque blending. Default `'okhsl'`. Ignored for `'transparent'` (always composites in linear sRGB). |
916
- | `contrast` | `HCPair<ContrastSpec>` | Optional contrast floor against `base` (WCAG or APCA — see [`contrast`](#contrast-floor)). The solver adjusts the mix ratio (opaque) or opacity (transparent). |
917
- | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
918
- | `role` | `RoleInput` | Semantic role of the mixed result against `base`. Same semantics as `RegularColorDef.role` (see [Roles](#roles)). |
919
- | `inherit` | `boolean` | Inheritance flag, default `true`. |
920
-
921
- See [Mix colors](#mix-colors) below.
922
-
923
- ***
924
-
925
- ## Standalone color tokens
926
-
927
- `glaze.color()` creates a single color token without a full theme.
928
-
929
- ```ts
930
- // arg1: the color (four shapes — see below)
931
- // arg2: optional config override (GlazeConfigOverride — see below)
932
- glaze.color(color: GlazeFromInput | GlazeColorInput | GlazeColorValue, config?: GlazeConfigOverride): GlazeColorToken;
933
- ```
934
-
935
- ### Input forms
936
-
937
- `glaze.color()` accepts **four input shapes**, discriminated by structure:
938
-
939
- | Shape | Example | Notes |
940
- | ---------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- |
941
- | **Bare string** | `'#26fcb2'` | Hex or CSS color function (`rgb()`, `hsl()`, `okhsl()`, `okhst()`, `oklch()`). |
942
- | **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). |
943
- | **`{ from, ...overrides }`** | `{ from: '#1a1a2e', base: bg, contrast: 'AA' }` | Value + color overrides in one object. |
944
- | **Structured** | `{ hue: 152, saturation: 95, tone: 74 }` | Full theme-style token (hue/saturation in 0–100, tone in 0–100). |
945
-
946
- `GlazeColorValue` (bare string or value-object forms) accepts:
947
-
948
- | Form | Example | Notes |
949
- | ------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
950
- | Hex | `'#26fcb2'`, `'#26fcb2ff'`, `'#abc'` | 3, 6, or 8 digits. Alpha is dropped with a `console.warn` — use `opacity` instead. |
951
- | `rgb()` | `'rgb(38 252 178)'`, `'rgb(38 252 178 / 0.8)'` | Modern space syntax. Alpha dropped with warning. |
952
- | `hsl()` | `'hsl(152 97% 57%)'` | Modern space syntax. Alpha dropped with warning. |
953
- | `okhsl()` | `'okhsl(152 95% 74%)'` | Glaze's own emit format. Alpha dropped with warning. |
954
- | `okhst()` | `'okhst(152 95% 70%)'` | OKHST tone input (third value is tone 0–100). Not native CSS; it can also be serialized by [Tasty](../../tasty/README.md) exports. Alpha dropped with warning. |
955
- | `oklch()` | `'oklch(0.85 0.18 152)'` | Glaze's own emit format. Alpha dropped with warning. |
956
- | `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. |
957
- | `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 }`. |
958
- | `RgbColor` object | `{ r: 38, g: 252, b: 178 }` | sRGB 0–255. RGB tuple `[r, g, b]` is not supported — use this object form. |
959
- | `OklchColor` object | `{ l: 0.85, c: 0.18, h: 152 }` | OKLCh (L/C: 0–1, H: degrees), same semantics as `oklch()` strings. |
960
-
961
- `GlazeColorInput` (structured form) is `{ hue, saturation, tone, ... }`:
962
-
963
- | Field | Type | Description |
964
- | ---------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
965
- | `hue` | `number` | 0–360. |
966
- | `saturation` | `number` | 0–100. |
967
- | `tone` | `HCPair<number \| ExtremeValue>` | 0–100 (contrast-shaped) or `'max'`/`'min'`, optional HC pair. |
968
- | `saturationFactor` | `number` | Multiplier on the seed (0–1). Default: `1`. |
969
- | `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`. |
970
- | `darkSaturation` | `number` | Dark-scheme seed saturation (0–100). Falls back to `saturation`. |
971
- | `darkSaturationFactor` | `number` | Dark-scheme multiplier on the dark seed (0–1). Falls back to `saturationFactor`. |
972
- | `mode` | `AdaptationMode` | Default: `'auto'`. |
973
- | `autoFlip` | `boolean` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
974
- | `opacity` | `number` | Fixed alpha 0–1. |
975
- | `base` | `GlazeColorToken \| GlazeColorValue` | Optional dependency. See [Pairing colors](#pairing-colors). |
976
- | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base` (WCAG or APCA). Without `base`, anchored to the literal seed. |
977
- | `pastel` | `boolean` | Per-color `pastel` override. Falls through to the per-theme / per-token `pastel` override when omitted. See [Per-color `pastel`](#per-color-pastel). |
978
- | `role` | `RoleInput` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
979
- | `name` | `string` | Debug label for warnings; doesn't change output keys. Reserved names (`'value'`, `'seed'`, `'externalBase'`) are rejected. |
980
-
981
- `GlazeFromInput` (from form) is `{ from: GlazeColorValue, ...colorOverrides }`:
982
-
983
- | Field | Notes |
984
- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
985
- | `from` | **Required.** The source color value — same forms as `GlazeColorValue`. |
986
- | `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed, never to `base`). |
987
- | `saturation` | Override seed saturation (0–100). |
988
- | `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. |
989
- | `saturationFactor` | Multiplier on the seed (0–1). |
990
- | `darkHue` | Dark-scheme hue: absolute (0–360) or `'+N'`/`'-N'` relative to the dark seed hue. Falls back to `hue`. |
991
- | `darkSaturation` | Dark-scheme seed saturation (0–100). Falls back to `saturation`. |
992
- | `darkSaturationFactor` | Dark-scheme multiplier on the dark seed (0–1). Falls back to `saturationFactor`. |
993
- | `mode` | `'auto'` (default) / `'fixed'` / `'static'`. |
994
- | `autoFlip` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
995
- | `contrast` | Contrast floor (WCAG or APCA). Without `base`, anchored to the literal seed; with `base`, solved per scheme. |
996
- | `base` | `GlazeColorToken` or raw `GlazeColorValue`. See [Pairing colors](#pairing-colors). |
997
- | `opacity` | Fixed alpha 0–1. Combining with `contrast` is not recommended — `console.warn` is emitted. |
998
- | `pastel` | Per-color `pastel` override. Falls through to the per-theme / per-token `pastel` override when omitted. See [Per-color `pastel`](#per-color-pastel). |
999
- | `role` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
1000
- | `name` | Debug label only — surfaces in warnings/errors. Does not change output keys. |
1001
-
1002
- Named CSS colors (`'red'`, `'blueviolet'`) are not supported.
1003
-
1004
- ### Defaults
1005
-
1006
- 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.
1007
-
1008
- * **Value-shorthand** (bare strings, value objects, and `{ from, ...overrides }`):
1009
- * Light variant preserves the input tone exactly (`lightTone: false` as a local default).
1010
- * Other omitted fields track the live global config.
1011
- * **Structured input** (`{ hue, saturation, tone, ... }`):
1012
- * Omitted tone windows and other fields track the live global config (same as a theme color).
1013
- * `pastel` is instance-only — set via the config override or per-color `pastel`, not `glaze.configure()`.
1014
-
1015
- ```ts
1016
- // Bare string — adapts automatically
1017
- glaze.color('#26fcb2');
1018
-
1019
- // Value-object — same behavior
1020
- glaze.color({ h: 152, s: 0.95, l: 0.74 });
1021
-
1022
- // OKHST value-object — tone axis
1023
- glaze.color({ h: 152, s: 0.95, t: 0.7 });
1024
-
1025
- // From form — value + color overrides
1026
- glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' });
1027
-
1028
- // Structured form — explicit hue/saturation/tone (0–100)
1029
- glaze.color({ hue: 152, saturation: 95, tone: 74 });
1030
- ```
1031
-
1032
- ### Token methods
1033
-
1034
- A `GlazeColorToken` exposes:
1035
-
1036
- | Method | Description |
1037
- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1038
- | `token.resolve()` | Resolve as a `ResolvedColor` (light/dark/lightContrast/darkContrast variants). |
1039
- | `token.token(options?)` | Flat token map (no color-name key). Options: `format`, `modes`, `states`. |
1040
- | `token.tasty(options?)` | [Tasty](../../tasty/README.md) state map (no color-name key). Same options as `token.token`. |
1041
- | `token.json(options?)` | JSON map (no color-name key). Options: `format`, `modes`. |
1042
- | `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`). |
1043
- | `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`. |
1044
- | `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`. |
1045
- | `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`. |
1046
- | `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). |
1047
-
1048
- ### Per-instance config override
1049
-
1050
- The optional `config` argument (`GlazeConfigOverride`) overrides
1051
- resolve-relevant fields for a token or theme. A tone window can be
1052
- `[lo, hi]`, `{ lo, hi, eps }`, or `false` (full range). Both themes and
1053
- standalone tokens keep a sparse local override — omitted fields (except
1054
- instance-only `pastel`) fall through to the live global at resolve time.
1055
-
1056
- `GlazeConfigOverride`:
1057
-
1058
- | Field | Default (from global / fixed) | Description |
1059
- | ------------------ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1060
- | `lightTone` | `[10, 100]` | Light tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
1061
- | `darkTone` | `[15, 95]` | Dark tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
1062
- | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
1063
- | `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. |
1064
- | `pastel` | `false` (instance-only) | Theme/token-level pastel default for colors that omit per-color `pastel`. Not available on `glaze.configure()`. |
1065
- | `inferRole` | `true` | Infer color `role` from the name when unset. |
1066
- | `shadowTuning` | `undefined` | Default shadow tuning (meaningful for themes; harmless on color tokens). |
1067
-
1068
- Config overrides apply to both `glaze.color()` tokens and `glaze()` themes:
1069
-
1070
- ```ts
1071
- // Standalone color — preserve raw tone in both schemes
1072
- glaze.color('#26fcb2', { darkTone: false });
1073
-
1074
- // Restore the #000 → white dark flip (full dark range)
1075
- glaze.color('#000000', {
1076
- lightTone: false,
1077
- darkTone: [15, 100],
1078
- });
1079
-
1080
- // Structured form with config override
1081
- glaze.color({ hue: 152, saturation: 95, tone: 74 }, { darkTone: false, pastel: true });
1082
-
1083
- // Theme with config override
1084
- const rawTheme = glaze(280, 80, { lightTone: false, pastel: true });
1085
- ```
1086
-
1087
- See [Theme config override](#theme-config-override).
1088
-
1089
- ### Theme config override
1090
-
1091
- When a theme is created with a `GlazeConfigOverride`, the override is **merged over the live global config at resolve time**. This means:
1092
-
1093
- * Fields you overrode are fixed — `glaze.configure()` can't change them for this theme.
1094
- * Fields you didn't override still react to later `glaze.configure()` calls.
1095
-
1096
- ```ts
1097
- const t = glaze(280, 80, { lightTone: [0, 50] });
1098
- t.colors({ text: { tone: 50, saturation: 1 } });
1099
- // text.light lands inside the [0, 50] window — always, regardless of
1100
- // global lightTone changes.
1101
- // text.dark.s reacts to glaze.configure({ darkDesaturation }) since it's not overridden.
1102
- ```
1103
-
1104
- `extend` inherits the parent's override and shallow-merges the child's:
1105
-
1106
- ```ts
1107
- const child = t.extend({ config: { darkTone: false } });
1108
- // child: lightTone { lo: 0, hi: 50 } (inherited) + darkTone: false (added)
1109
- ```
1110
-
1111
- `theme.export(override?)` freezes `getConfig() ∪ instance local ∪ override` at
1112
- call time. Restoring via `glaze.themeFrom(data)` (or the compat alias
1113
- `glaze.from`) pins that freeze as the restored theme's local override — matching
1114
- standalone color-token behavior.
1115
-
1116
- ### `glaze.colorFrom(data)`
1117
-
1118
- Inverse of `token.export()`. The exported snapshot includes the original input,
1119
- all overrides (with any `base` token recursively serialized), and the effective
1120
- config freeze from export time — so later `glaze.configure()` calls don't change
1121
- rehydrated tokens.
1122
-
1123
- ```ts
1124
- const text = glaze.color({ from: '#1a1a1a', contrast: 'AA' });
1125
- const data = text.export();
1126
- const restored = glaze.colorFrom(data);
1127
- // restored.resolve() === text.resolve() byte-for-byte
1128
- ```
1129
-
1130
- Both value-form and structured-form tokens round-trip.
1131
-
1132
- ### Pairing colors
1133
-
1134
- 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.
1135
-
1136
- ```ts
1137
- const bg = glaze.color('#1a1a2e');
1138
-
1139
- // Text guaranteed AA against `bg` in every scheme.
1140
- const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
1141
-
1142
- // Border 8 tone units lighter than `bg` in each scheme.
1143
- const border = glaze.color({
1144
- from: '#000000',
1145
- base: bg,
1146
- tone: '+8',
1147
- mode: 'fixed',
1148
- });
1149
-
1150
- // Raw-value base — Glaze auto-wraps it via `glaze.color(value)`.
1151
- const text2 = glaze.color({ from: '#ffffff', base: '#1a1a2e', contrast: 'AA' });
1152
- ```
1153
-
1154
- Behavior with `base`:
1155
-
1156
- * `contrast` is solved per scheme against `base`'s resolved variant (light / dark / lightContrast / darkContrast).
1157
- * Relative `tone: '+N'` / `'-N'` is anchored to `base`'s tone per scheme (matches theme behavior).
1158
- * Relative `hue: '+N'` / `'-N'` still anchors to the **seed** (the value passed to `glaze.color()`), not the base.
1159
- * `mode` works as a per-pair knob.
1160
- * 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.
1161
- * **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.
1162
- * 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.
1163
-
1164
- Chains compose:
1165
-
1166
- ```ts
1167
- const bg = glaze.color('#000000');
1168
- const surface = glaze.color({ from: '#222222', base: bg, contrast: 'AAA' });
1169
- const text = glaze.color({ from: '#ffffff', base: surface, contrast: 'AA' });
1170
- ```
1171
-
1172
- ### `name` is a debug label
1173
-
1174
- 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.
1175
-
1176
- ***
1177
-
1178
- ## Shadows
1179
-
1180
- ### Defining shadow colors in a theme
1181
-
1182
- ```ts
1183
- theme.colors({
1184
- surface: { tone: 95 },
1185
- text: { base: 'surface', tone: '-52', contrast: 'AAA' },
1186
-
1187
- 'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
1188
- 'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
1189
- 'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
1190
- });
1191
- ```
1192
-
1193
- Shadow colors are included in all output methods (`tokens()`, `tasty()`, `css()`, `json()`) alongside regular colors and emit an alpha component:
1194
-
1195
- ```
1196
- 'oklch(0.15 0.009 282 / 0.1)'
1197
- 'rgb(34 28 42 / 0.1)'
1198
- ```
1199
-
1200
- ### How shadows work
1201
-
1202
- 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.
1203
- 2. **Pigment color** — hue blended between fg and bg, low saturation, dark lightness.
1204
- 3. **Alpha** — computed via a `tanh` curve that saturates smoothly toward `alphaMax` (default `1.0`), ensuring well-separated shadow levels even on dark backgrounds.
1205
-
1206
- Omit `fg` for an achromatic shadow at full user-specified intensity:
1207
-
1208
- ```ts
1209
- theme.colors({
1210
- 'drop-shadow': { type: 'shadow', bg: 'surface', intensity: 12 },
1211
- });
1212
- ```
1213
-
1214
- `intensity` supports `[normal, highContrast]` pairs:
1215
-
1216
- ```ts
1217
- 'shadow-card': { type: 'shadow', bg: 'surface', fg: 'text', intensity: [10, 20] },
1218
- ```
1219
-
1220
- ### `ShadowTuning`
1221
-
1222
- Fine-tune behavior per-color or globally via `glaze.configure({ shadowTuning })`. Per-color `tuning` is merged field-by-field with the global one.
1223
-
1224
- | Parameter | Default | Description |
1225
- | ------------------ | -------------- | ------------------------------------------------------------------------------------- |
1226
- | `saturationFactor` | `0.18` | Fraction of fg saturation kept in pigment. |
1227
- | `maxSaturation` | `0.25` | Upper clamp on pigment saturation. |
1228
- | `lightnessFactor` | `0.25` | Multiplier for bg lightness → pigment lightness. |
1229
- | `lightnessBounds` | `[0.05, 0.20]` | Clamp range for pigment lightness. |
1230
- | `minGapTarget` | `0.05` | Target minimum gap between pigment and bg lightness. |
1231
- | `alphaMax` | `1.0` | Asymptotic maximum alpha. |
1232
- | `bgHueBlend` | `0.2` | Blend weight pulling pigment hue toward bg hue. `0` = pure fg hue, `1` = pure bg hue. |
1233
-
1234
- ```ts
1235
- theme.colors({
1236
- 'shadow-soft': {
1237
- type: 'shadow',
1238
- bg: 'surface',
1239
- intensity: 10,
1240
- tuning: { alphaMax: 0.3, saturationFactor: 0.1 },
1241
- },
1242
- });
1243
-
1244
- glaze.configure({
1245
- shadowTuning: { alphaMax: 0.5, bgHueBlend: 0.3 },
1246
- });
1247
- ```
1248
-
1249
- ### Standalone shadow computation
1250
-
1251
- `glaze.shadow(input)` computes a shadow outside of a theme. `bg` and `fg` accept any `GlazeColorValue`:
1252
-
1253
- ```ts
1254
- const v = glaze.shadow({
1255
- bg: '#f0eef5',
1256
- fg: '#1a1a2e',
1257
- intensity: 10,
1258
- });
1259
- // → { h: 280, s: 0.14, l: 0.2, alpha: 0.1 }
1260
-
1261
- const css = glaze.format(v, 'oklch');
1262
- // → 'oklch(0.15 0.014 280 / 0.1)'
1263
- ```
1264
-
1265
- `GlazeShadowInput`:
1266
-
1267
- | Field | Type | Description |
1268
- | ----------- | ----------------- | ------------------------------------------------------------------------------ |
1269
- | `bg` | `GlazeColorValue` | Background. Any `GlazeColorValue` form. Alpha components dropped with warning. |
1270
- | `fg` | `GlazeColorValue` | Optional foreground. Same forms as `bg`. |
1271
- | `intensity` | `number` | 0–100. |
1272
- | `tuning` | `ShadowTuning` | Optional. |
1273
-
1274
- ### Fixed opacity (regular colors)
1275
-
1276
- For a simple fixed-alpha color (no shadow algorithm), use `opacity` on a regular color:
1277
-
1278
- ```ts
1279
- theme.colors({
1280
- overlay: { tone: 0, opacity: 0.5 },
1281
- });
1282
- // → 'oklch(0 0 0 / 0.5)'
1283
- ```
1284
-
1285
- ***
1286
-
1287
- ## Mix colors
1288
-
1289
- ### Opaque mix
1290
-
1291
- Produces a solid color by interpolating between `base` and `target`:
1292
-
1293
- ```ts
1294
- theme.colors({
1295
- surface: { tone: 95 },
1296
- accent: { tone: 30 },
1297
- tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
1298
- });
1299
- ```
1300
-
1301
- * `value: 0` = pure base, `value: 100` = pure target.
1302
- * Result has alpha = 1.
1303
- * Adapts to light/dark/HC schemes automatically via the resolved base and target.
1304
-
1305
- ### Transparent mix
1306
-
1307
- Produces the target color with controlled opacity — useful for hover overlays:
1308
-
1309
- ```ts
1310
- theme.colors({
1311
- surface: { tone: 95 },
1312
- black: { tone: 0, saturation: 0 },
1313
- hover: {
1314
- type: 'mix',
1315
- base: 'surface',
1316
- target: 'black',
1317
- value: 8,
1318
- blend: 'transparent',
1319
- },
1320
- });
1321
- // hover → black with alpha = 0.08
1322
- ```
1323
-
1324
- The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
1325
-
1326
- ### Blend space (opaque only)
1327
-
1328
- | `space` | Behavior | Best for |
1329
- | ------------------- | ----------------------------------------- | -------------------------------------------------------- |
1330
- | `'okhsl'` (default) | Perceptually uniform OKHSL interpolation. | Design token derivation. |
1331
- | `'srgb'` | Linear sRGB channel interpolation. | Matching browser compositing of CSS color-mix / overlay. |
1332
-
1333
- Transparent blending always composites in linear sRGB (matches browser alpha compositing).
1334
-
1335
- ### Contrast solving on mixes
1336
-
1337
- 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:
1338
-
1339
- ```ts
1340
- 'tint': {
1341
- type: 'mix', base: 'surface', target: 'accent',
1342
- value: 10, contrast: 'AA',
1343
- },
1344
- 'overlay': {
1345
- type: 'mix', base: 'surface', target: 'accent',
1346
- value: 5, blend: 'transparent', contrast: 3,
1347
- },
1348
- ```
1349
-
1350
- Both `value` and `contrast` support `[normal, highContrast]` pairs.
1351
-
1352
- ### Achromatic colors
1353
-
1354
- 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.
1355
-
1356
- ### Mix chaining
1357
-
1358
- Mix colors can reference other mix colors:
1359
-
1360
- ```ts
1361
- theme.colors({
1362
- white: { tone: 100, saturation: 0 },
1363
- black: { tone: 0, saturation: 0 },
1364
- gray: {
1365
- type: 'mix',
1366
- base: 'white',
1367
- target: 'black',
1368
- value: 50,
1369
- space: 'srgb',
1370
- },
1371
- lightGray: {
1372
- type: 'mix',
1373
- base: 'white',
1374
- target: 'gray',
1375
- value: 50,
1376
- space: 'srgb',
1377
- },
1378
- });
1379
- ```
1380
-
1381
- Mix colors **cannot** reference shadow colors (same restriction as regular dependent colors).
1382
-
1383
- ***
1384
-
1385
- ## Palette
1386
-
1387
- `glaze.palette(themes, options?)` composes multiple themes into a single token namespace.
1388
-
1389
- ```ts
1390
- const palette = glaze.palette({ primary, danger, success, warning });
1391
- const palette = glaze.palette(
1392
- { primary, danger, success },
1393
- { primary: 'primary' },
1394
- );
1395
- ```
1396
-
1397
- `GlazePaletteOptions`:
1398
-
1399
- | Option | Description |
1400
- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1401
- | `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. |
1402
-
1403
- A `GlazePalette` exposes:
1404
-
1405
- | Method | Description |
1406
- | -------------------------------- | ------------------------------------------------------------------------------------------------- |
1407
- | `palette.list()` | Theme names in insertion order. |
1408
- | `palette.primary` | Primary theme name, if set. |
1409
- | `palette.theme(name)` | Get a live theme instance by name. |
1410
- | `palette.themes()` | Shallow copy of the theme map (same instances the palette holds). |
1411
- | `palette.export(override?)` | Authoring snapshot — restorable via `glaze.paletteFrom()`. Optional override forwarded to themes. |
1412
- | `palette.tokens(options?)` | Flat token map grouped by scheme variant. |
1413
- | `palette.tasty(options?)` | [Tasty](../../tasty/README.md) style-to-state bindings. |
1414
- | `palette.json(options?)` | Per-theme **resolved** color JSON (not restorable as authoring config). |
1415
- | `palette.css(options?)` | CSS custom property declaration strings. |
1416
- | `palette.dtcg(options?)` | Per-scheme W3C DTCG token trees. |
1417
- | `palette.dtcgResolver(options?)` | One DTCG Resolver-Module document for every scheme. |
1418
- | `palette.tailwind(options?)` | One Tailwind CSS v4 theme with scheme overrides. |
1419
-
1420
- ### `palette.export(override?)` / `glaze.paletteFrom()`
1421
-
1422
- ```ts
1423
- const snapshot = palette.export();
1424
- // → {
1425
- // kind: 'palette',
1426
- // version: 1,
1427
- // primary: 'brand',
1428
- // themes: { brand: { kind: 'theme', ... }, danger: { ... } },
1429
- // }
1430
-
1431
- const restored = glaze.paletteFrom(JSON.parse(JSON.stringify(snapshot)));
1432
- const brand = restored.theme('brand')!;
1433
- ```
1434
-
1435
- Optional `override` is forwarded to each nested `theme.export(override)`.
1436
- Config snapshot vs resolved output: use `export()` / `paletteFrom()` to
1437
- persist and restore the authoring graph (themes, color defs, relations). Use
1438
- `json()` / `tokens()` / `css()` / … to emit resolved color strings for apps
1439
- and design tools.
1440
-
1441
- ### `GlazePaletteExportOptions`
1442
-
1443
- Shared by `tokens`, `tasty`, and `css`:
1444
-
1445
- | Option | Default | Description |
1446
- | --------- | ------------------------------ | -------------------------------------------------------------------------------------------- |
1447
- | `prefix` | `true` (= `"<themeName>-"`) | `false` disables prefixing. Or pass a custom map: `{ primary: 'brand-', danger: 'error-' }`. |
1448
- | `primary` | inherits from palette creation | `string` to override, `false` to disable for this call. |
1449
-
1450
- Each export method also accepts its own format/options shape:
1451
-
1452
- | Method | Additional options |
1453
- | -------------------------------- | ----------------------------------------- |
1454
- | `palette.tokens(options?)` | `format`, `modes` |
1455
- | `palette.tasty(options?)` | `format`, `modes`, `states` |
1456
- | `palette.css(options?)` | `format`, `suffix` |
1457
- | `palette.dtcg(options?)` | `colorSpace`, `modes` |
1458
- | `palette.dtcgResolver(options?)` | `colorSpace`, `modes`, resolver names |
1459
- | `palette.tailwind(options?)` | `format`, `modes`, selectors, `namespace` |
1460
-
1461
- `palette.css()` does not accept `modes`; it always returns all four CSS strings (`light`, `dark`, `lightContrast`, `darkContrast`).
1462
-
1463
- ### Prefix behavior
1464
-
1465
- By default all palette tokens are prefixed:
1466
-
1467
- ```ts
1468
- palette.tokens();
1469
- // → {
1470
- // light: { 'primary-surface': 'oklch(...)', 'danger-surface': 'oklch(...)' },
1471
- // dark: { 'primary-surface': 'oklch(...)', 'danger-surface': 'oklch(...)' },
1472
- // }
1473
- ```
1474
-
1475
- Custom map (any theme not listed falls back to `"<themeName>-"`):
1476
-
1477
- ```ts
1478
- palette.tokens({ prefix: { primary: 'brand-', danger: 'error-' } });
1479
- ```
1480
-
1481
- Disable prefixing:
1482
-
1483
- ```ts
1484
- palette.tokens({ prefix: false });
1485
- ```
1486
-
1487
- ### Collision detection
1488
-
1489
- 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:
1490
-
1491
- ```
1492
- glaze: token "surface" from theme "b" collides with theme "a" — skipping.
1493
- ```
1494
-
1495
- ### Primary theme aliases
1496
-
1497
- The primary theme's tokens are duplicated without prefix:
1498
-
1499
- ```ts
1500
- const palette = glaze.palette(
1501
- { primary, danger, success },
1502
- { primary: 'primary' },
1503
- );
1504
- palette.tokens();
1505
- // → {
1506
- // light: {
1507
- // 'primary-surface': 'oklch(...)',
1508
- // 'danger-surface': 'oklch(...)',
1509
- // 'success-surface': 'oklch(...)',
1510
- // 'surface': 'oklch(...)', // unprefixed alias
1511
- // },
1512
- // }
1513
- ```
1514
-
1515
- Override per-export:
1516
-
1517
- ```ts
1518
- palette.tokens({ primary: 'danger' });
1519
- palette.tokens({ primary: false });
1520
- ```
1521
-
1522
- The primary alias works alongside any prefix mode — when using a custom map, primary tokens are still duplicated without prefix:
1523
-
1524
- ```ts
1525
- palette.tokens({ prefix: { primary: 'p-', danger: 'd-' } });
1526
- // → 'p-surface' + 'surface' (alias) + 'd-surface'
1527
- ```
1528
-
1529
- ### `palette.json()`
1530
-
1531
- JSON export groups by theme name (no prefix needed):
1532
-
1533
- ```ts
1534
- palette.json();
1535
- // → {
1536
- // primary: { surface: { light: 'oklch(...)', dark: 'oklch(...)' } },
1537
- // danger: { surface: { light: 'oklch(...)', dark: 'oklch(...)' } },
1538
- // }
1539
- ```
1540
-
1541
- ### `palette.css()`
1542
-
1543
- ```ts
1544
- const css = palette.css();
1545
- const stylesheet = `
1546
- :root { ${css.light} }
1547
- @media (prefers-color-scheme: dark) {
1548
- :root { ${css.dark} }
1549
- }
1550
- `;
1551
- ```
1552
-
1553
- `palette.css()` accepts the same `GlazeCssOptions` as `theme.css()` plus `GlazePaletteExportOptions`.
1554
- It does not accept `modes`; all four result fields are always returned.
1555
-
1556
- ### `palette.dtcg()`
1557
-
1558
- 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).
1559
-
1560
- ```ts
1561
- palette.dtcg();
1562
- // → {
1563
- // light: {
1564
- // 'primary-surface': { $type: 'color', $value: { ... } },
1565
- // 'surface': { $type: 'color', $value: { ... } }, // unprefixed alias
1566
- // 'danger-surface': { $type: 'color', $value: { ... } },
1567
- // },
1568
- // dark: { ... },
1569
- // }
1570
- ```
1571
-
1572
- Accepts `GlazeDtcgOptions` plus `GlazePaletteExportOptions`.
1573
-
1574
- ### `palette.dtcgResolver()`
1575
-
1576
- 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.
1577
-
1578
- ```ts
1579
- palette.dtcgResolver();
1580
- // → {
1581
- // version: '2025.10',
1582
- // sets: { base: { sources: [ { 'primary-surface': {…}, 'surface': {…}, 'danger-surface': {…} } ] } },
1583
- // modifiers: { scheme: { default: 'light', contexts: { light: [], dark: [ {…} ] } } },
1584
- // resolutionOrder: [ { $ref: '#/sets/base' }, { $ref: '#/modifiers/scheme' } ],
1585
- // }
1586
- ```
1587
-
1588
- Accepts `GlazeDtcgResolverOptions` plus `GlazePaletteExportOptions`.
1589
-
1590
- ### `palette.tailwind()`
1591
-
1592
- 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`.
1593
-
1594
- ```ts
1595
- const css = palette.tailwind();
1596
- // @theme {
1597
- // --color-primary-surface: oklch(...);
1598
- // --color-surface: oklch(...); /* unprefixed alias */
1599
- // --color-danger-surface: oklch(...);
1600
- // }
1601
- // .dark { ... }
1602
- ```
1603
-
1604
- Accepts `GlazeTailwindOptions` plus `GlazePaletteExportOptions`. The palette `prefix` option (theme prefixing) is separate from `GlazeTailwindOptions.namespace` (the `--color-*` CSS namespace).
1605
-
1606
- ***
1607
-
1608
- ## Output formats
1609
-
1610
- Control the color format with the `format` option on any export method:
1611
-
1612
- | Format | Output (alpha = 1) | Output (alpha < 1) | Notes |
1613
- | ------------------------------------------ | ------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------- |
1614
- | `'oklch'` (default for CSS-string exports) | `oklch(L C H)` | `oklch(L C H / A)` | OKLab-based LCH. Native CSS. Required for `splitHue`. |
1615
- | `'rgb'` | `rgb(R G B)` | `rgb(R G B / A)` | Rounded integers, modern space syntax. |
1616
- | `'hsl'` | `hsl(H S% L%)` | `hsl(H S% L% / A)` | Modern space syntax. |
1617
- | `'okhsl'` | `okhsl(H S% L%)` | `okhsl(H S% L% / A)` | Glaze's native format, not a CSS function. **[Tasty](../../tasty/README.md)-only** (`tasty()`, `token()`, `.tasty()`). |
1618
- | `'okhst'` | `okhst(H S% T%)` | `okhst(H S% T% / A)` | OKHST tone axis. **[Tasty](../../tasty/README.md)-only** — same restriction as `okhsl`. |
1619
-
1620
- ```ts
1621
- theme.tokens(); // 'oklch(0.965 0.0123 280)' (default)
1622
- theme.tokens({ format: 'rgb' }); // 'rgb(244 240 250)'
1623
- theme.tasty(); // 'oklch(0.965 0.0123 280)' (default)
1624
- theme.tasty({ format: 'okhst' }); // 'okhst(280 60% 97%)'
1625
- ```
1626
-
1627
- All numeric output strips trailing zeros for cleaner CSS (e.g. `95` not `95.0`).
1628
-
1629
- 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](../../tasty/README.md) exports** (`tokens`, `json`, `css`, `tailwind`) — they are not native CSS color spaces.
1630
-
1631
- ### Hue channel splitting (`splitHue`)
1632
-
1633
- 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:
1634
-
1635
- ```css
1636
- /* theme.css({ format: 'oklch', splitHue: true, name: 'brand' }) */
1637
- --brand-hue: 240;
1638
- --accent-hue: calc(var(--brand-hue) + 20);
1639
- --surface-color: oklch(0.52 0.06 var(--brand-hue));
1640
- --accent-color: oklch(0.62 0.03 var(--accent-hue));
1641
- ```
1642
-
1643
- **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.
1644
-
1645
- **Dark hues:** hue vars are scheme-independent unless the theme or a color authors a [dark hue](#dark-seed-darkhue--darksaturation). When one does, the whole hue set is re-declared in the dark block (and in the Tasty dark state, gated by `modes.dark`):
1646
-
1647
- ```css
1648
- /* glaze({ hue: 240, saturation: 18, darkHue: 200 }) */
1649
- :root {
1650
- --brand-hue: 240;
1651
- --accent-hue: calc(var(--brand-hue) + 20);
1652
- }
1653
- /* dark */
1654
- --brand-hue: 200;
1655
- --accent-hue: calc(var(--brand-hue) + 20);
1656
- ```
1657
-
1658
- `--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.
1659
-
1660
- **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).
1661
-
1662
- `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.
1663
-
1664
- ***
1665
-
1666
- ## Adaptation modes
1667
-
1668
- `mode` controls how a color adapts across schemes:
1669
-
1670
- | Mode | Behavior |
1671
- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
1672
- | `'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. |
1673
- | `'fixed'` | Color stays recognizable. Tone is *mapped* (not inverted) into the dark window. Use for brand buttons, CTAs, status banners. |
1674
- | `'static'` | No adaptation. Same tone in every scheme. |
1675
-
1676
- ### How relative tone adapts
1677
-
1678
- **`auto`** — the offset is anchored to the base's per-scheme tone:
1679
-
1680
- ```
1681
- Light: surface tone=97, text tone='-52' → tone 45 (dark text on light bg)
1682
- Dark: surface inverts to a low tone; the '-52' offset re-anchors to the
1683
- base's light tone and maps into the dark window (light text on dark bg)
1684
- ```
1685
-
1686
- **`fixed`** — tone is mapped (not inverted), relative sign preserved:
1687
-
1688
- ```
1689
- Light: accent-fill tone=52, accent-text tone='+20' → lighter than the fill
1690
- Dark: accent-fill maps into the dark window, sign preserved
1691
- ```
1692
-
1693
- 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.
1694
-
1695
- **`static`** — no adaptation, same tone in every scheme.
1696
-
1697
- ***
1698
-
1699
- ## Light / dark scheme mapping
1700
-
1701
- The mapping is a single tone pipeline; there is no Möbius curve. See
1702
- [Scheme adaptation](okhst.md#scheme-adaptation) for the product-level model and
1703
- the [canonical OKHST specification](https://github.com/tenphi/okhst) for the
1704
- transfer math.
1705
-
1706
- ### Light scheme
1707
-
1708
- An authored tone (0–100) is remapped into the `lightTone` **tone window**.
1709
- The window's `lo`/`hi` are OKHSL-lightness boundaries (0–100); authored tone is
1710
- positioned within the corresponding tone interval and converted to final OKHSL
1711
- lightness. `static` mode and HC variants use the full range.
1712
-
1713
- ```
1714
- window = lightTone // default [10, 100]
1715
- finalTone = remap(authorTone, window)
1716
- finalL = fromTone(finalTone) // OKHSL lightness
1717
- ```
1718
-
1719
- ### Dark scheme
1720
-
1721
- **`auto`** — invert the tone, then remap into the dark window:
1722
-
1723
- ```
1724
- window = darkTone // default [15, 95]
1725
- inverted = 100 - authorTone
1726
- finalTone = remap(inverted, window)
1727
- ```
1728
-
1729
- The inversion preserves authored tone spacing without a fitted curve. This is
1730
- exactly contrast-even for neutrals and approximate for chromatic colors. The
1731
- ordinary light/dark asymmetry lives in the two windows' `(lo, hi, eps)` values
1732
- (`eps` defaults to the reference `0.05`).
1733
-
1734
- **`fixed`** — remap into the dark window without inversion:
1735
-
1736
- ```
1737
- finalTone = remap(authorTone, darkTone)
1738
- ```
1739
-
1740
- In high-contrast variants both windows are bypassed (forced to the full `[0, 100]` range): `auto` still inverts, `fixed`/`static` do not.
1741
-
1742
- ### Dark scheme — saturation
1743
-
1744
- `darkDesaturation` reduces saturation for all colors in dark scheme:
1745
-
1746
- ```ts
1747
- S_dark = S_light * (1 - darkDesaturation); // default: 0.1
1748
- ```
1749
-
1750
- `static` mode skips desaturation.
1751
-
1752
- ***
1753
-
1754
- ## Configuration
1755
-
1756
- ```ts
1757
- glaze.configure({
1758
- lightTone: [10, 100], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
1759
- darkTone: [15, 95], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
1760
- darkDesaturation: 0.1,
1761
- states: {
1762
- dark: '@media(prefers-color-scheme: dark)',
1763
- highContrast: '@media(prefers-contrast: more)',
1764
- },
1765
- modes: {
1766
- dark: true,
1767
- highContrast: false,
1768
- },
1769
- shadowTuning: {
1770
- alphaMax: 0.6,
1771
- bgHueBlend: 0.2,
1772
- },
1773
- contrastLevel: 'auto', // or 0–100 for a manual contrast slider
1774
- });
1775
- ```
1776
-
1777
- A `ToneWindow` is `[lo, hi]` (OKHSL-lightness boundaries, reference eps — the
1778
- common form), `{ lo, hi, eps }` (advanced: explicit per-scheme render eps), or
1779
- `false` for the full range `[0, 100]` at the reference eps. `false` removes the
1780
- boundaries, not the tone transfer.
1781
-
1782
- `GlazeConfig`:
1783
-
1784
- | Field | Default | Description |
1785
- | --------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1786
- | `lightTone` | `[10, 100]` | Light scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC; widened continuously by [`contrastLevel`](#manual-contrast-level). |
1787
- | `darkTone` | `[15, 95]` | Dark scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC; widened continuously by [`contrastLevel`](#manual-contrast-level). |
1788
- | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
1789
- | `states.dark` | `'@media(prefers-color-scheme: dark)'` | State alias for dark mode tokens ([Tasty](../../tasty/README.md) export). Defaults to a media query so tokens react to the OS preference without registering custom states. |
1790
- | `states.highContrast` | `'@media(prefers-contrast: more)'` | State alias for HC tokens ([Tasty](../../tasty/README.md) export). |
1791
- | `modes.dark` | `true` | Include dark variants in exports. |
1792
- | `modes.highContrast` | `false` | Include HC variants. Independent of [`contrastLevel`](#manual-contrast-level) — 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. |
1793
- | `shadowTuning` | `undefined` | Default tuning for all shadow colors. Per-color tuning merges field-by-field. |
1794
- | `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`](#autoflip). |
1795
- | `inferRole` | `true` | Infer each color's [`role`](#roles) 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). |
1796
- | `contrastLevel` | `'auto'` | Manual contrast level, `0`–`100`, 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](#manual-contrast-level). |
1797
-
1798
- | Method | Description |
1799
- | ------------------------- | ----------------------------------------------------------------------------------- |
1800
- | `glaze.configure(config)` | Merge into the global config. Bumps a config version that invalidates theme caches. |
1801
- | `glaze.getConfig()` | Snapshot the current resolved config (shallow copy). |
1802
- | `glaze.resetConfig()` | Reset to defaults (also bumps the version counter). |
1803
-
1804
- 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](#clearing-it-exporting-it-base-links)).
1805
-
1806
- ***
1807
-
1808
- ## Output modes
1809
-
1810
- Control which scheme variants appear in `tokens()` / `tasty()` / `json()` exports:
1811
-
1812
- ```ts
1813
- // Light only
1814
- palette.tokens({ modes: { dark: false, highContrast: false } });
1815
-
1816
- // Light + dark (default)
1817
- palette.tokens({ modes: { highContrast: false } });
1818
-
1819
- // All four variants
1820
- palette.tokens({ modes: { dark: true, highContrast: true } });
1821
- // → { light, dark, lightContrast, darkContrast }
1822
- ```
1823
-
1824
- Resolution priority (highest first):
1825
-
1826
- 1. A global [`contrastLevel`](#manual-contrast-level) of `100` — pins
1827
- `highContrast: false`, since the normal variants already *are* the
1828
- high-contrast ones and the tier would duplicate them. Any other level leaves
1829
- `highContrast` alone.
1830
- 2. Per-call `modes` option on `tokens` / `tasty` / `json`.
1831
- 3. `glaze.configure({ modes })` — global config.
1832
- 4. Built-in default: `{ dark: true, highContrast: false }`.
1833
-
1834
- ***
1835
-
1836
- ## Validation
1837
-
1838
- Invalid definitions throw before resolution when Glaze cannot produce a
1839
- well-defined dependency graph. Recoverable numeric bounds are clamped. A
1840
- physically unreachable contrast floor or a potentially misleading
1841
- contrast/opacity combination emits `console.warn` and returns the closest
1842
- available result.
1843
-
1844
- | Condition | Behavior |
1845
- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1846
- | `contrast` without `base` in a **theme** color | Validation error |
1847
- | `contrast` pair switching metric (`[4.5, { apca: 75 }]`) | Validation error — a WCAG ratio and an APCA Lc are different scales |
1848
- | Relative `tone` without `base` in a **theme** color | Validation error |
1849
- | `contrast` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1850
- | Relative `tone` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1851
- | 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. |
1852
- | `tone` resolves outside 0–100 | Clamp silently |
1853
- | `'max'` / `'min'` without `base` | Allowed — resolves to the scheme's tone extreme (root color) |
1854
- | `saturation` outside 0–1 | Clamp silently |
1855
- | Circular `base` references | Validation error |
1856
- | `base` references non-existent name | Validation error |
1857
- | Shadow `bg` references non-existent color | Validation error |
1858
- | Shadow `fg` references non-existent color | Validation error |
1859
- | Shadow `bg` references another shadow color | Validation error |
1860
- | Shadow `fg` references another shadow color | Validation error |
1861
- | Regular color `base` references a shadow color | Validation error |
1862
- | Shadow `intensity` outside 0–100 | Clamp silently |
1863
- | `contrast` + `opacity` combined | `console.warn` |
1864
- | Mix `base` references non-existent color | Validation error |
1865
- | Mix `target` references non-existent color | Validation error |
1866
- | Mix `base` references a shadow color | Validation error |
1867
- | Mix `target` references a shadow color | Validation error |
1868
- | Mix `value` outside 0–100 | Clamp silently |
1869
- | Circular references involving mix colors | Validation error |
1870
- | Contrast target physically unreachable | `console.warn` (deduped per `(name, scheme, target)`); closest passing variant returned |
1871
-
1872
- ***
1873
-
1874
- ## Color math utilities
1875
-
1876
- For advanced use, Glaze re-exports its internal color math.
1877
-
1878
- ### Conversions
1879
-
1880
- ```ts
1881
- import {
1882
- okhslToLinearSrgb,
1883
- okhslToSrgb,
1884
- okhslToOklab,
1885
- oklabToOkhsl,
1886
- srgbToOkhsl,
1887
- hslToSrgb,
1888
- parseHex,
1889
- parseHexAlpha,
1890
- relativeLuminanceFromLinearRgb,
1891
- contrastRatioFromLuminance,
1892
- gamutClampedLuminance,
1893
- } from '@tenphi/glaze';
1894
- ```
1895
-
1896
- | Function | Description |
1897
- | ------------------------------------- | ------------------------------------------------------------------------ |
1898
- | `okhslToLinearSrgb(h, s, l)` | OKHSL (h: 0–360, s/l: 0–1) → linear sRGB tuple. |
1899
- | `okhslToSrgb(h, s, l)` | OKHSL (h: 0–360, s/l: 0–1) → gamma-encoded sRGB tuple (0–1 per channel). |
1900
- | `okhslToOklab([h, s, l])` | OKHSL → OKLab `[L, a, b]`. |
1901
- | `oklabToOkhsl([L, a, b])` | OKLab → OKHSL. |
1902
- | `srgbToOkhsl([r, g, b])` | Gamma sRGB (0–1) → OKHSL. |
1903
- | `hslToSrgb(h, s, l)` | CSS HSL → sRGB tuple. |
1904
- | `parseHex(hex)` | Parse `#rgb` / `#rrggbb` to sRGB tuple. Returns `null` on invalid input. |
1905
- | `parseHexAlpha(hex)` | Parse `#rgb` / `#rrggbb` / `#rrggbbaa`; returns `[r, g, b, a?]`. |
1906
- | `relativeLuminanceFromLinearRgb(rgb)` | WCAG relative luminance from linear sRGB. |
1907
- | `contrastRatioFromLuminance(yA, yB)` | WCAG contrast ratio from two luminances. |
1908
- | `gamutClampedLuminance(linearRgb)` | Relative luminance with channel clamping for out-of-gamut colors. |
1909
-
1910
- ### Format writers
1911
-
1912
- Every writer takes `h` on 0–360 and `s` / `l` / `t` on **0–1** — the scale every
1913
- conversion above *returns*, and the scale `resolve()` stores in a
1914
- `ResolvedColorVariant`. The percentages are an output detail: the writers scale
1915
- by 100 themselves where the CSS syntax asks for one. (The one thing on a
1916
- different scale is the pair of tone transfers below, `toTone` / `fromTone`,
1917
- which speak the 0–100 tone axis the authoring API takes.)
1918
-
1919
- ```ts
1920
- import {
1921
- formatOkhsl,
1922
- formatOkhst,
1923
- formatRgb,
1924
- formatHsl,
1925
- formatOklch,
1926
- } from '@tenphi/glaze';
1927
-
1928
- formatOkhsl(280, 0.6, 0.95); // 'okhsl(280 60% 95%)'
1929
- formatOkhst(280, 0.6, 0.95); // 'okhst(280 60% 95%)'
1930
- formatRgb(280, 0.6, 0.95); // 'rgb(238.45 239.95 251.1)'
1931
- formatHsl(280, 0.6, 0.95); // 'hsl(232.92 61.87% 95.99%)'
1932
- formatOklch(280, 0.6, 0.95); // 'oklch(0.9571 0.015 280)'
1933
- ```
1934
-
1935
- So a producer composes with a writer directly, with nothing to rescale in
1936
- between:
1937
-
1938
- ```ts
1939
- const v = glaze.color('#7A4DBF').resolve().light;
1940
- const { h, s, l } = variantToOkhsl(v);
1941
-
1942
- formatOkhst(v.h, v.s, v.t); // 'okhst(298.52 70.41% 45.02%)'
1943
- formatOkhsl(h, s, l); // 'okhsl(298.52 70.41% 45.27%)'
1944
- formatRgb(h, s, l); // 'rgb(122 77 191)'
1945
- ```
1946
-
1947
- A value above 1 can only be pre-2.0 percentage-scale input, so the writers
1948
- `console.warn` about it once per writer rather than emit a wrong color quietly.
1949
-
1950
- To attach an alpha component, use `glaze.format(variant, format)` on a `ResolvedColorVariant` (which carries the `alpha` channel) instead of these raw writers.
1951
-
1952
- ### OKHST tone utilities
1953
-
1954
- ```ts
1955
- import {
1956
- toTone,
1957
- fromTone,
1958
- toneFromY,
1959
- yFromTone,
1960
- okhstToOkhsl,
1961
- okhslToOkhst,
1962
- variantToOkhsl,
1963
- REF_EPS,
1964
- } from '@tenphi/glaze';
1965
- ```
1966
-
1967
- | Function | Description |
1968
- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
1969
- | `toTone(l, eps?)` | OKHSL lightness (0–1) → tone (**0–100**, the authoring scale — divide by 100 for `formatOkhst`). Defaults to `REF_EPS`. |
1970
- | `fromTone(t, eps?)` | Tone (**0–100**) → OKHSL lightness (0–1). Inverse of `toTone`. |
1971
- | `toneFromY(y, eps?)` / `yFromTone(t, eps?)` | Same transfer in luminance space (0–1). |
1972
- | `okhstToOkhsl({ h, s, t })` | OKHST → OKHSL (`{ h, s, l }`). |
1973
- | `okhslToOkhst({ h, s, l })` | OKHSL → OKHST (`{ h, s, t }`). |
1974
- | `variantToOkhsl(variant)` | `ResolvedColorVariant` (stores `t`) → `{ h, s, l }` for rendering (`alpha` stays on the variant). |
1975
- | `REF_EPS` | Reference epsilon (`0.05`) for the canonical tone axis. |
1976
-
1977
- `ResolvedColorVariant` stores `{ h, s, t, alpha }` (tone, not lightness). Use
1978
- `variantToOkhsl(variant).l` to recover OKHSL lightness. See
1979
- [OKHST in Glaze](okhst.md) for the model.
1980
-
1981
- ### Contrast solver
1982
-
1983
- ```ts
1984
- import {
1985
- findToneForContrast,
1986
- findValueForMixContrast,
1987
- resolveContrastForMode,
1988
- resolveContrastForLevel,
1989
- resolveMinContrast,
1990
- apcaContrast,
1991
- } from '@tenphi/glaze';
1992
- ```
1993
-
1994
- | Function | Description |
1995
- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1996
- | `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? }`. |
1997
- | `findValueForMixContrast(opts)` | Same, but searches for a mix `value` (0–1) that meets a contrast floor between a base and a target. |
1998
- | `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). |
1999
- | `resolveContrastForLevel(spec, level, polarity?)` | Resolves a `ContrastSpec` — including its outer `[normal, highContrast]` pair — at a [manual contrast level](#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. |
2000
- | `resolveMinContrast(value)` | Resolves a `MinContrast` (WCAG preset or number) to a numeric ratio. |
2001
- | `apcaContrast(yText, yBg)` | APCA Lc magnitude (0–106) for two relative luminances. |
2002
-
2003
- Exported constants: `APCA_PRESETS`, `APCA_HC_ENHANCEMENT` (`15`, the Enhanced Level delta), `APCA_MAX_LC` (`106`).
2004
-
2005
- `findToneForContrast` options:
2006
-
2007
- | Option | Default | Description |
2008
- | ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2009
- | `hue` | — | Candidate hue (0–360). |
2010
- | `saturation` | — | Candidate saturation (0–1). |
2011
- | `preferredTone` | — | Preferred candidate tone (0–1). Kept if it already meets the target. |
2012
- | `baseLinearRgb` | — | Base color as linear sRGB tuple. |
2013
- | `contrast` | — | `ResolvedContrast` (`{ metric, target }`). |
2014
- | `toneRange` | `[0, 1]` | Search bounds in tone. |
2015
- | `epsilon` | `1e-4` | Convergence threshold. |
2016
- | `maxIterations` | `18` | Max binary-search iterations per branch. |
2017
- | `initialDirection` | higher-contrast side | Direction to search first (`'lighter'` or `'darker'`). |
2018
- | `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. |
2019
- | `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`](#manual-contrast-level) 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. |
2020
-
2021
- Result: `{ tone, contrast, met, branch: 'lighter' | 'darker' | 'preferred', flipped? }`. `flipped: true` indicates the initial direction failed and the opposite direction satisfied the target.