@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
@@ -0,0 +1,922 @@
1
+ ---
2
+ title: Designing a palette
3
+ description: Build a complete Glaze palette step by step, from the surface ladder to status themes, and see it grow in every scheme.
4
+ ---
5
+ import { Callout, Steps } from '@tenphi/cookbook/components';
6
+ import PaletteStep from '../docs-site/components/methodology/PaletteStep.tsx';
7
+ import PalettePreview from '../docs-site/components/methodology/PalettePreview.tsx';
8
+ import StatusThemes from '../docs-site/components/methodology/StatusThemes.tsx';
9
+ import PaletteOutput from '../docs-site/components/methodology/PaletteOutput.astro';
10
+ import { PURPLE_HUE } from '../docs-site/components/methodology/palette.ts';
11
+
12
+ A practical way to design a Glaze palette without fighting dark mode. Start
13
+ from tone relationships, add contrast only where a role needs a readable floor,
14
+ and let `extend()` carry the same decisions across status hues.
15
+
16
+ This guide builds one palette from scratch: a neutral default theme with
17
+ surfaces, text, accents, disabled states, and effects, then a set of status
18
+ themes extended from it. After each step you see the tokens it adds and the
19
+ product UI built from the palette so far, resolved live in the scheme you are
20
+ viewing. Switch the Appearance menu to check dark and high contrast as you go.
21
+ This is where the guide ends up:
22
+
23
+ <PalettePreview caption="The finished palette, in the scheme you are viewing. Every part of this UI appears in the step that defines its token." />
24
+
25
+ For the color model behind tone, see [Tone and OKHST](./okhst.mdx); the
26
+ derivation lives in the
27
+ [canonical OKHST specification](https://github.com/tenphi/okhst). Every option
28
+ used here is documented in the [API reference](./reference/index.md).
29
+
30
+ ## The mental model
31
+
32
+ Glaze palettes work best as one default neutral theme plus a few colored
33
+ sibling themes:
34
+
35
+ * `default` owns the neutral system most components consume: `#surface`,
36
+ `#surface-text`, `#border`, `#disabled-surface`, and so on.
37
+ * Status themes (`success`, `danger`, `warning`, `note`, …) are created with
38
+ `extend()`. They swap the hue and keep only the inherited tokens that should
39
+ become status-aware.
40
+ * `inherit: false` keeps a token local to the parent theme. Use it for neutral
41
+ ladders, shadows, code colors, scrims, and anything that should not be
42
+ repeated for every status hue.
43
+
44
+ The main simplification is OKHST tone. Dark mode is a single inversion
45
+ (`100 - t`) plus a scheme tone window, so tone relationships carry over from
46
+ light to dark. A relative **tone delta** such as `tone: '-4'` is an exact tone
47
+ step from its base in light. In dark it steps from where the base sits on the
48
+ authored scale and is inverted and remapped like any authored tone, so it flips
49
+ with its base and keeps its place in the ladder, slightly compressed by the
50
+ dark window (about 0.87× at the defaults; high contrast keeps it exact). The
51
+ step is exactly contrast-even for neutrals and approximate for chromatic
52
+ colors; add a `contrast` floor when the measured result matters.
53
+ [How relative tone adapts](./schemes.mdx#how-relative-tone-adapts) shows the
54
+ mapping.
55
+
56
+ Use `glaze.color()` instead of a theme when you need one standalone color or
57
+ one base/dependent pair and do not need a named palette. The same tone,
58
+ adaptation, and contrast rules apply; see
59
+ [Color tokens](./reference/color-tokens.md).
60
+
61
+ ## Implementation workflow
62
+
63
+ Build in dependency order so every decision has a clear base:
64
+
65
+ <Steps>
66
+ <li>Configure the output schemes and application states once.</li>
67
+ <li>Choose the default theme's hue and saturation seed.</li>
68
+ <li>Define root surfaces with absolute tones.</li>
69
+ <li>Add dependent surfaces, text, borders, and icons with tone deltas.</li>
70
+ <li>Add contrast floors only to roles that need measured readability or recognizability.</li>
71
+ <li>Choose adaptation per color: `auto`, `fixed`, or `static`.</li>
72
+ <li>Add high-contrast pairs to tones and effects that should separate more in high contrast.</li>
73
+ <li>Mark default-only tokens `inherit: false`, then extend the shared definitions into status themes.</li>
74
+ <li>Compose and export the palette in the shape your application consumes.</li>
75
+ <li>Verify complete screens in all emitted scheme variants.</li>
76
+ </Steps>
77
+
78
+ The sections below follow this order and build one palette incrementally.
79
+
80
+ ## Authoring decisions
81
+
82
+ Use this order when defining a token:
83
+
84
+ 1. Pick the base it visually belongs to.
85
+ 2. Use an absolute numeric `tone` for independent placement. Use a signed
86
+ **tone delta** (`'+N'` / `'-N'`) for distance from a base: surface ladders,
87
+ soft chips, disabled states, and similar relationships. Under `auto`, the
88
+ dark step mirrors the light one around its base, wherever the base sits,
89
+ but a `fixed` base doesn't invert while the delta does, so the delta lands
90
+ on the other side of it in dark: make a delta off a `fixed` base `fixed`
91
+ too when it should keep its side. A delta has no floor of its own, so give
92
+ a hover that must stay readable its own `contrast`, or use a
93
+ [state layer](#state-layers) on fills.
94
+ 3. Add `contrast` only when readability or recognizability needs a measured
95
+ floor.
96
+ 4. Prefer APCA presets for content-like colors when perceptual readability is
97
+ the design goal: `contrast: { apca: 'content' }`.
98
+ 5. Use WCAG numbers or presets when compatibility, policy, or migration
99
+ requires a WCAG ratio: `contrast: 4.5`, `contrast: 'AAA'`, or
100
+ `contrast: { wcag: [4.5, 7] }`.
101
+ 6. Let token names infer APCA roles. A name whose last recognized keyword is
102
+ `text`, `label`, `border`, `surface`, `fill`, `bg`, or a similar alias
103
+ already tells Glaze which side is foreground or background, so
104
+ `surface-text-soft` is text and `accent-surface-hover` is a surface. Set
105
+ `role` only when a name is ambiguous.
106
+ 7. Let high contrast escalate floors on its own: an APCA floor rises by Lc 15,
107
+ `'AA'` becomes `'AAA'`, and `'AA-large'` becomes `'AAA-large'`. Add a
108
+ high-contrast pair where a value does not escalate by itself (tone, shadow /
109
+ glow intensity, mix value, a bare WCAG number) or where you want a different
110
+ high-contrast target.
111
+
112
+ [Contrast solving](./contrast.mdx) explains the two metrics and the presets;
113
+ [Roles and APCA polarity](./contrast.mdx#roles-and-apca-polarity) covers name
114
+ inference.
115
+
116
+ ### Choosing adaptation mode
117
+
118
+ Pick the mode by role. This palette uses all three:
119
+
120
+ | Mode | Roles |
121
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
122
+ | `auto` (default) | Surfaces, text, borders, icons, and accent foregrounds on neutral UI: they trade places with their background in dark. |
123
+ | `fixed` | Accent fills, their label, and the inverse surface: they keep their side of the tone scale. |
124
+ | `static` | The modal scrim, which must stay pure black in every scheme. |
125
+
126
+ [Adaptation modes](./schemes.mdx#adaptation-modes) shows what each mode does
127
+ per scheme. Dark tone inversion is controlled by `mode` alone; `autoFlip` only
128
+ lets an overshooting tone delta or an unmet contrast direction reverse around
129
+ its base.
130
+
131
+ ### Root or dependent?
132
+
133
+ * Use a root color when its tone has meaning on its own: the page surface, a
134
+ fixed brand anchor, or a scheme extreme.
135
+ * Use a dependent color when its purpose exists relative to another token:
136
+ text on a surface, a border around a fill, or a tint of an accent.
137
+ * Setting `base` with an absolute tone is valid when the color needs a
138
+ contrast relationship but not a tone delta. The absolute position is resolved
139
+ independently; `contrast` acts as a safety floor. Without `contrast`, such a
140
+ base does not change the resolved color; it only records which surface the
141
+ color belongs to.
142
+
143
+ ## Seed and configure
144
+
145
+ Keep hue decisions named and configure output modes once:
146
+
147
+ ```ts
148
+ import { glaze } from '@tenphi/glaze';
149
+
150
+ const PURPLE_HUE = 280.3;
151
+ const SUCCESS_HUE = 156.9;
152
+ const DANGER_HUE = 23.1;
153
+ const WARNING_HUE = 84.3;
154
+ const NOTE_HUE = 302.3;
155
+
156
+ const SEED_SATURATION = 80;
157
+
158
+ glaze.configure({
159
+ states: { dark: '@dark', highContrast: '@hc' },
160
+ modes: { dark: true, highContrast: true },
161
+ });
162
+ ```
163
+
164
+ `states` names the selectors your app uses for dark and high contrast, and
165
+ `modes` turns on the variants Glaze emits; both are covered in
166
+ [Configuration](./reference/configuration.md).
167
+
168
+ Per-color `saturation` is a factor of the theme seed, not an absolute
169
+ saturation. With `SEED_SATURATION = 80`, `saturation: 0.25` means one quarter
170
+ of that seed.
171
+
172
+ ### When dark needs its own chroma
173
+
174
+ Dark schemes get a flat `darkDesaturation` haircut (10% by default), which is
175
+ enough for most palettes. Reach for a second seed only when dark is wrong in a
176
+ way a uniform reduction cannot fix — a hue that reads muddy against dark
177
+ surfaces, or an accent that stays gaudy no matter how much you dial the global
178
+ knob. Pass the dark seed with the object form when you create the theme:
179
+
180
+ ```ts
181
+ const defaultTheme = glaze({
182
+ hue: PURPLE_HUE,
183
+ saturation: SEED_SATURATION,
184
+ darkHue: PURPLE_HUE - 12, // cooler purple on dark surfaces
185
+ darkSaturation: 62, // calmer than a 10% haircut would give
186
+ });
187
+ ```
188
+
189
+ <PalettePreview
190
+ columns={[
191
+ { label: 'Dark · seed only', scheme: 'dark' },
192
+ {
193
+ label: 'Dark · with the dark seed',
194
+ scheme: 'dark',
195
+ darkHue: PURPLE_HUE - 12,
196
+ darkSaturation: 62,
197
+ },
198
+ ]}
199
+ caption="The finished palette in dark, without and with the dark seed above. The accent fill's dark saturation goes from 72 (the seed's 80 less the 10% haircut) to exactly 62."
200
+ />
201
+
202
+ An explicit `darkSaturation` replaces `darkDesaturation` rather than stacking
203
+ with it, so the number you write is the number you get. Prefer tuning the seed
204
+ over per-color `darkHue` / `darkSaturation`: one seed keeps the palette
205
+ coherent, while scattered per-color dark overrides drift apart as the palette
206
+ grows. Save the per-color form for the one or two tokens that genuinely need
207
+ it. [Dark seed](./schemes.mdx#dark-seed) shows the effect per scheme, and
208
+ [Color definitions](./reference/color-definitions.md) has the per-color rules.
209
+ The rest of this guide builds the default theme without a dark seed, so its
210
+ previews show the plain haircut.
211
+
212
+ ## Naming
213
+
214
+ Prefer purpose first and variant last:
215
+
216
+ * Surfaces: `surface`, `surface-2`, `surface-3`.
217
+ * Foregrounds: `surface-text`, `surface-text-soft`, `surface-text-soft-2`.
218
+ * Structure: `border`, `divider`, `outline`, `placeholder`, `focus`.
219
+ * Fills: `accent-surface`, `accent-surface-2`, `accent-surface-hover`.
220
+ * Foregrounds on neutral surfaces: `accent-text`, `accent-text-soft`,
221
+ `accent-icon`.
222
+ * Disabled states: `disabled-surface`, `disabled-surface-text`,
223
+ `accent-disabled-surface`, `accent-disabled-surface-text`.
224
+ * Effects: `shadow-sm`, `shadow-md`, `shadow-lg`, `surface-hover`,
225
+ `accent-surface-pressed`, `accent-surface-highlight`, `scrim`, `hover`,
226
+ `tint`.
227
+
228
+ These names are not only readable. They also help APCA role inference pick the
229
+ right polarity. For example, `button-text` is foreground, `input-bg` is a
230
+ surface, and `card-outline` is a border.
231
+
232
+ ## Build the default theme
233
+
234
+ ### The surface ladder
235
+
236
+ Start with the surface family. It is mostly tone, with small saturation changes
237
+ to keep the ladder visually coherent:
238
+
239
+ ```ts
240
+ const defaultTheme = glaze(PURPLE_HUE, SEED_SATURATION);
241
+
242
+ defaultTheme.colors({
243
+ surface: { tone: 100, saturation: 0.11 },
244
+ 'surface-2': {
245
+ base: 'surface',
246
+ tone: '-2',
247
+ saturation: 0.15,
248
+ inherit: false,
249
+ },
250
+ 'surface-3': {
251
+ base: 'surface',
252
+ tone: '-4',
253
+ saturation: 0.19,
254
+ inherit: false,
255
+ },
256
+ });
257
+ ```
258
+
259
+ <PaletteStep step="surfaces" schemes={['light', 'dark']} caption="The ladder in light and dark. Each step moves away from surface's end of the scale in both schemes. Before any text exists, the UI is a wireframe: the page is surface-2, the cards are surface, and the bars and track are surface-3." />
260
+
261
+ Each tone delta is an exact step from `surface` in light; in dark it steps from
262
+ `surface`'s authored tone and is mapped like an authored tone, so the ladder
263
+ keeps its order everywhere and flips with `surface` in dark. The dark window
264
+ compresses the steps a little: `'-2'` and `'-4'` land 1.7 and 3.5 tones from
265
+ `surface` in dark, and high contrast keeps them exact. These small deltas are
266
+ enough to define the ladder; there is no separate dark-mode curve to tune.
267
+
268
+ ### Text and borders
269
+
270
+ Use a hard edge tone for maximum-prominence text, and APCA floors for softer
271
+ content:
272
+
273
+ ```ts
274
+ defaultTheme.colors({
275
+ 'surface-text': {
276
+ base: 'surface',
277
+ tone: 2,
278
+ saturation: 0.475,
279
+ },
280
+ 'surface-text-soft': {
281
+ base: 'surface',
282
+ tone: '-1',
283
+ saturation: 0.375,
284
+ contrast: { apca: ['content', 'body'] },
285
+ inherit: false,
286
+ },
287
+ 'surface-text-soft-2': {
288
+ base: 'surface',
289
+ tone: '-1',
290
+ saturation: 0.24,
291
+ contrast: { apca: ['large', 'content'] },
292
+ inherit: false,
293
+ },
294
+ border: {
295
+ base: 'surface',
296
+ tone: ['-10', '-20'],
297
+ saturation: 0.175,
298
+ inherit: false,
299
+ },
300
+ });
301
+ ```
302
+
303
+ <PaletteStep step="text" caption="Each foreground on its base, with the APCA Lc it reaches against surface in your scheme. The soft variants land on their floors (Lc 60 and 45, or 75 and 60 in high contrast). The border has no floor; APCA rates very dark pairs at Lc 0, so in dark it reads 0 although it sits about 9 tones off surface (20 in high contrast)." />
304
+
305
+ `surface-text` uses an absolute `tone: 2`: it is intentionally edge-anchored,
306
+ and with no `contrast` floor its `base` only records which surface it belongs
307
+ to. The soft variants use a `-1` tone delta as the preferred direction and
308
+ APCA as the readable floor. Their pairs only spell out what high contrast does
309
+ anyway (an APCA floor rises by Lc 15), so a bare `{ apca: 'content' }`
310
+ resolves the same; this guide writes them out to keep the high-contrast target
311
+ visible. `border` needs its tone pair, because tones do not escalate on their
312
+ own and borders usually need a larger visible step in high contrast.
313
+
314
+ Repeat the same pattern for `surface-2` and `surface-3` only if components need
315
+ text directly on those surfaces.
316
+
317
+ ### Neutral utility tokens
318
+
319
+ Keep neutral-only primitives local to the default theme:
320
+
321
+ ```ts
322
+ defaultTheme.colors({
323
+ placeholder: {
324
+ base: 'surface',
325
+ tone: 67,
326
+ saturation: 0.175,
327
+ inherit: false,
328
+ },
329
+ focus: {
330
+ base: 'surface',
331
+ tone: 71,
332
+ saturation: 0.8625,
333
+ inherit: false,
334
+ },
335
+ disabled: {
336
+ tone: 80.8,
337
+ saturation: 0.4,
338
+ inherit: false,
339
+ },
340
+ });
341
+ ```
342
+
343
+ <PaletteStep step="utility" caption="A search field with its placeholder and focus ring, and an unavailable option drawn in disabled." />
344
+
345
+ Absolute tones are fine for primitives whose job is visual placement rather
346
+ than a strict relationship to a specific surface.
347
+
348
+ ## Chips and disabled states
349
+
350
+ For subtle fills, tone is usually clearer than contrast:
351
+
352
+ ```ts
353
+ defaultTheme.colors({
354
+ 'disabled-surface': {
355
+ base: 'surface',
356
+ tone: '-3',
357
+ saturation: 0.2,
358
+ inherit: false,
359
+ },
360
+ 'disabled-surface-text': {
361
+ base: 'disabled-surface',
362
+ tone: '-18',
363
+ saturation: 0.3,
364
+ autoFlip: false,
365
+ inherit: false,
366
+ },
367
+ });
368
+ ```
369
+
370
+ <PaletteStep step="chips" caption="A disabled chip: a small tone step off surface, with a deliberately faint label 16 to 18 tones from the chip in every scheme. In dark its Lc drops toward 0 for the same reason the border's does." />
371
+
372
+ This says exactly what the pair should do: the chip sits a small tone delta off
373
+ `surface`, and the label sits a muted delta from the chip. `autoFlip: false`
374
+ keeps the label on the authored side when the delta reaches the edge. Point the
375
+ delta away from the nearer edge: the chip sits at tone 97 in light, so `'+18'`
376
+ would have only three tones of room, and with `autoFlip: false` it would clamp
377
+ at 100 and the label would all but vanish.
378
+
379
+ Use contrast instead when the chip must hit an explicit accessibility floor:
380
+
381
+ ```ts
382
+ defaultTheme.colors({
383
+ 'disabled-surface-text': {
384
+ base: 'disabled-surface',
385
+ tone: '+1',
386
+ saturation: 0.3,
387
+ contrast: { apca: 'non-text' },
388
+ inherit: false,
389
+ },
390
+ });
391
+ ```
392
+
393
+ <PaletteStep step="chips" extra="disabled-floor" preview={false} caption="The same chip with the floor instead: the label reaches the same APCA Lc in light and dark (30, raised automatically to 45 in high contrast). The '+1' preference is too close to meet it, so the solver crosses to the other side of the chip." />
394
+
395
+ When a token needs the scheme extreme, use `tone: 'min'` or `tone: 'max'`
396
+ directly. Avoid large magic numbers or fake contrast floors just to push a
397
+ color to the edge.
398
+
399
+ Pair the extreme with a `base` when the gap between the two has to survive the
400
+ scheme switch. Glaze then replays the light-scheme tone shift against the
401
+ resolved dark base instead of re-mapping the extreme through the dark window,
402
+ so a knockout on a surface keeps the same contrast in both schemes.
403
+ [Extremes in dark](./schemes.mdx#extremes-in-dark) shows the replay.
404
+
405
+ ## Fixed surfaces and accent fills
406
+
407
+ Use `mode: 'fixed'` when the authored color should stay recognizable across
408
+ schemes.
409
+
410
+ ```ts
411
+ defaultTheme.colors({
412
+ 'surface-inverse': {
413
+ tone: 12,
414
+ saturation: 0.475,
415
+ mode: 'fixed',
416
+ inherit: false,
417
+ },
418
+
419
+ 'accent-surface-text': {
420
+ tone: 100,
421
+ mode: 'fixed',
422
+ },
423
+ 'accent-surface': {
424
+ base: 'accent-surface-text',
425
+ tone: '-1',
426
+ contrast: { apca: ['content', 'body'] },
427
+ mode: 'fixed',
428
+ },
429
+ 'accent-surface-2': {
430
+ base: 'accent-surface-text',
431
+ tone: '-1',
432
+ contrast: { apca: [65, 80] },
433
+ mode: 'fixed',
434
+ },
435
+ 'accent-surface-hover': {
436
+ base: 'accent-surface-text',
437
+ tone: '-1',
438
+ contrast: { apca: ['body', 'preferred'] },
439
+ mode: 'fixed',
440
+ },
441
+ });
442
+ ```
443
+
444
+ <PaletteStep step="fixed" schemes={['light', 'dark']} caption="Fixed tokens keep their side of the scale: surface-inverse stays dark and the accent fills keep a light label in both schemes. Lc is measured with accent-surface-text as the label on each fill." />
445
+
446
+ The accent fill family is a fixed chain against a fixed text anchor. The names
447
+ infer `surface` and `text` roles, so APCA gets the right polarity without extra
448
+ fields.
449
+
450
+ ## Adaptive accent foregrounds
451
+
452
+ Brand foregrounds that sit on neutral surfaces should stay adaptive:
453
+
454
+ ```ts
455
+ defaultTheme.colors({
456
+ 'accent-text': {
457
+ base: 'surface',
458
+ tone: '-1',
459
+ saturation: 1,
460
+ contrast: { apca: ['content', 'body'] },
461
+ },
462
+ 'accent-text-soft': {
463
+ base: 'surface',
464
+ tone: '-1',
465
+ saturation: 1,
466
+ contrast: { apca: ['large', 'content'] },
467
+ },
468
+ 'accent-icon': {
469
+ base: 'surface',
470
+ tone: '-1',
471
+ saturation: 0.9375,
472
+ contrast: { apca: ['non-text', 'large'] },
473
+ },
474
+ });
475
+ ```
476
+
477
+ <PaletteStep step="adaptive" caption="A link, its softer caption, and an icon, each floored against surface." />
478
+
479
+ Anchor these to `surface`, not to `accent-surface`. Their real job is to remain
480
+ readable on neutral UI, so `mode: 'auto'` and a surface base are the right
481
+ defaults.
482
+
483
+ Brand-tinted disabled states can usually be pure tone:
484
+
485
+ ```ts
486
+ defaultTheme.colors({
487
+ 'accent-disabled-surface': {
488
+ base: 'surface',
489
+ tone: '+3',
490
+ saturation: 0.5,
491
+ },
492
+ 'accent-disabled-surface-text': {
493
+ base: 'accent-disabled-surface',
494
+ tone: '-18',
495
+ saturation: 0.4,
496
+ autoFlip: false,
497
+ },
498
+ });
499
+ ```
500
+
501
+ <PaletteStep step="accent-disabled" caption="The brand-tinted disabled button." />
502
+
503
+ On the white default `surface` there is no room for `'+3'`, so `autoFlip`
504
+ (on by default) mirrors the chip just below `surface`; on the tinted status
505
+ surfaces later in this guide it lands above. The label follows the same rule as
506
+ the neutral disabled chip.
507
+
508
+ These are inherited, so status themes automatically get
509
+ `success-accent-disabled-surface`, `danger-accent-disabled-surface`, and the
510
+ matching text tokens.
511
+
512
+ ## Special-purpose colors
513
+
514
+ Use absolute `hue` overrides for tokens that should come from another hue
515
+ family but keep the same adaptation behavior:
516
+
517
+ ```ts
518
+ defaultTheme.colors({
519
+ 'code-comment': {
520
+ base: 'surface',
521
+ hue: 280,
522
+ saturation: 0.1,
523
+ tone: '-1',
524
+ contrast: { apca: ['large', 'content'] },
525
+ inherit: false,
526
+ },
527
+ 'code-keyword': {
528
+ base: 'surface',
529
+ hue: 348,
530
+ saturation: 1,
531
+ tone: '-1',
532
+ contrast: { apca: ['content', 'body'] },
533
+ inherit: false,
534
+ },
535
+ 'code-string': {
536
+ base: 'surface',
537
+ hue: SUCCESS_HUE,
538
+ saturation: 1,
539
+ tone: '-1',
540
+ contrast: { apca: ['large', 'content'] },
541
+ inherit: false,
542
+ },
543
+ });
544
+ ```
545
+
546
+ <PaletteStep step="code" caption="Syntax colors from three hue families, each floored against surface like any other foreground." />
547
+
548
+ Use small tone ramps for decorative motion:
549
+
550
+ ```ts
551
+ defaultTheme.colors({
552
+ 'loading-face-1': {
553
+ base: 'surface',
554
+ tone: 98,
555
+ saturation: 0.3,
556
+ inherit: false,
557
+ },
558
+ 'loading-face-2': {
559
+ base: 'surface',
560
+ tone: 91,
561
+ saturation: 0.62,
562
+ inherit: false,
563
+ },
564
+ 'loading-face-3': {
565
+ base: 'surface',
566
+ tone: 79,
567
+ saturation: 0.66,
568
+ inherit: false,
569
+ },
570
+ });
571
+ ```
572
+
573
+ <PaletteStep step="loading" caption="A loading skeleton built from the three faces." />
574
+
575
+ Absolute tones invert and remap the same way in every scheme, so the ramp keeps
576
+ its order and close to its spacing without involving the contrast solver: the
577
+ tone windows narrow it to about 95% in light and 87% in dark, and high contrast
578
+ keeps it exact. Use a high-contrast tone pair only when the animation should
579
+ become more pronounced in high contrast.
580
+
581
+ ## Effects
582
+
583
+ Shadows and glows are translucent overlays with an exact tone step:
584
+ `intensity` is how many tones darker (`shadow`) or lighter (`glow`) the surface
585
+ ends up once the browser composites it. The opacity is solved per surface, so
586
+ the same number reads the same on white, on a mid-tone accent and on a dark
587
+ surface. Direction is fixed per type: a `shadow` always darkens and a `glow`
588
+ always lightens, in every scheme. [Shadows and glows](./shadows-and-glows.mdx)
589
+ covers the model in depth.
590
+
591
+ ### Shadows
592
+
593
+ Define one neutral shadow system. Point `bg` at the surface the shadow falls on
594
+ and `fg` at the element that casts it — here a `surface` card on the
595
+ `surface-2` page:
596
+
597
+ ```ts
598
+ defaultTheme.colors({
599
+ 'shadow-sm': {
600
+ type: 'shadow',
601
+ bg: 'surface-2',
602
+ fg: 'surface',
603
+ intensity: 4,
604
+ inherit: false,
605
+ },
606
+ 'shadow-md': {
607
+ type: 'shadow',
608
+ bg: 'surface-2',
609
+ fg: 'surface',
610
+ intensity: [8, 14],
611
+ inherit: false,
612
+ },
613
+ 'shadow-lg': {
614
+ type: 'shadow',
615
+ bg: 'surface-2',
616
+ fg: 'surface',
617
+ intensity: [12, 20],
618
+ inherit: false,
619
+ },
620
+ });
621
+ ```
622
+
623
+ <PaletteStep step="shadows" schemes={['light', 'dark', 'lightContrast', 'darkContrast']} preview={false} caption="A surface card casting each shadow on surface-2, in all four schemes. The value line is the solved opacity and the tone step the composite actually lands." />
624
+
625
+ `fg` modulates the step the way bounce light does: an object at the surface's
626
+ own tone gives exactly `intensity`, a darker object casts a deeper shadow and a
627
+ lighter one a softer shadow. Do not point `fg` at text — it is the object, not
628
+ a contrast reference. Use a high-contrast pair for shadows that should deepen
629
+ in high contrast.
630
+
631
+ <Callout kind="caution" title="Shadows need room below the surface">
632
+ A shadow can only darken as far as black. The page surface here,
633
+ `surface-2`, sits at tone 98 in light but about 11 in dark and 2 in dark
634
+ high contrast, so `shadow-lg` falls just short of its step in dark, and in
635
+ dark high contrast all three pin at full opacity and still land only about 2
636
+ tones down. Glaze warns for each step it cannot reach. Every color resolves
637
+ in all four schemes, so not binding a shadow in dark only keeps the pinned
638
+ value off screen; the warning fires anyway. The fix is a lower `intensity`,
639
+ or a surface with room below it in every scheme. For elevation
640
+ in dark, reach for [glows](./shadows-and-glows.mdx#dark-mode-elevation)
641
+ instead. This palette keeps its near-white page, so these warnings are
642
+ expected.
643
+ </Callout>
644
+
645
+ ### State layers
646
+
647
+ Without `fg`, a shadow or glow is an exact shade or tint — the right tool for
648
+ state layers, which need the same visible change on every fill:
649
+
650
+ ```ts
651
+ defaultTheme.colors({
652
+ 'surface-hover': { type: 'shadow', bg: 'surface', intensity: 3 },
653
+ 'accent-surface-pressed': {
654
+ type: 'shadow',
655
+ bg: 'accent-surface',
656
+ intensity: [6, 10],
657
+ },
658
+ 'accent-surface-highlight': {
659
+ type: 'glow',
660
+ bg: 'accent-surface',
661
+ intensity: 5,
662
+ },
663
+ });
664
+ ```
665
+
666
+ <PaletteStep step="state-layers" caption="Each layer over the fill it targets, then in the UI: the hovered row and the pressed and highlighted buttons. The cards now carry shadow-md and shadow-sm." />
667
+
668
+ A fixed opacity cannot do this: an 8% black layer darkens white by about 6
669
+ tones but a tone-10 surface by about 1
670
+ ([The same step on every surface](./shadows-and-glows.mdx#the-same-step-on-every-surface)
671
+ compares the two). The same limit applies, though: in dark high
672
+ contrast `surface` sits at tone 0, so `surface-hover` has nothing to darken
673
+ there and resolves transparent, with a warning.
674
+
675
+ Glows are the mirror, for dark-mode elevation and edge highlights. A glow needs
676
+ room above its surface, so a glow on a white light-mode surface has nothing to
677
+ lighten — Glaze leaves it transparent and warns, even if you only use the token
678
+ in dark. Give glows a surface with room in every scheme, as
679
+ `accent-surface-highlight` has on the mid-tone accent fill. With `headroom`
680
+ above 0, an [HDR glow](./hdr.mdx) can carry a value past white for platforms
681
+ that can show it, but its standard value on white stays transparent.
682
+
683
+ ### Fixed opacity and mixes
684
+
685
+ Use `opacity` for one fixed-alpha color, such as a modal scrim:
686
+
687
+ ```ts
688
+ defaultTheme.colors({
689
+ scrim: {
690
+ tone: 0,
691
+ saturation: 0,
692
+ mode: 'static',
693
+ opacity: 0.5,
694
+ inherit: false,
695
+ },
696
+ });
697
+ ```
698
+
699
+ <PaletteStep step="scrim" schemes={['light', 'dark']} preview={false} caption="The scrim at a fixed 50% over surface: black in light and dark, and in both high-contrast variants." />
700
+
701
+ `mode: 'static'` keeps the scrim pure black. Under the default `auto`, tone 0
702
+ would be remapped to tone 4.95 in light and inverted to 95.9 in dark, turning
703
+ the scrim into a light 50% wash there.
704
+
705
+ Use mixes when one color should tint through another:
706
+
707
+ ```ts
708
+ defaultTheme.colors({
709
+ hover: {
710
+ type: 'mix',
711
+ base: 'surface',
712
+ target: 'accent-surface',
713
+ value: 8,
714
+ blend: 'transparent',
715
+ },
716
+ tint: {
717
+ type: 'mix',
718
+ base: 'surface',
719
+ target: 'accent-surface',
720
+ value: 20,
721
+ },
722
+ });
723
+ ```
724
+
725
+ <PaletteStep step="mixes" caption="A transparent accent hover over surface and a solid 20% tint, then the complete default theme." />
726
+
727
+ Transparent mixes are good for tinted hover washes. Opaque mixes are good for
728
+ solid tints. Opaque mixes default to perceptual OKHSL interpolation; set
729
+ `space: 'srgb'` for linear-light channel interpolation. Transparent mixes are
730
+ composited by the browser, and their contrast solve models that compositing.
731
+ Mix colors can also use `contrast`; the solver adjusts the value or opacity to
732
+ meet the floor. If the floor is out of reach, the mix settles at the pure target
733
+ (the pure base when its value is already 100) and Glaze warns, as it does for a
734
+ regular color. See [Mixes and transparency](./mixes.mdx).
735
+
736
+ ## High contrast
737
+
738
+ High contrast is not a separate palette. Contrast floors escalate on their own
739
+ in the high-contrast variants: an APCA floor rises by Lc 15, `'AA'` becomes
740
+ `'AAA'`, and `'AA-large'` becomes `'AAA-large'` (a bare WCAG number stays as
741
+ written). Text, icons, and accent fills tighten with no extra fields;
742
+ [Contrast solving](./contrast.mdx#high-contrast) has the full table.
743
+
744
+ Tones, shadow / glow `intensity`, and mix `value` do not escalate. Give them a
745
+ high-contrast pair where users should get more separation:
746
+
747
+ * Borders: `tone: ['-10', '-20']`.
748
+ * Shadows and glows: `intensity: [8, 14]`.
749
+ * Decorative ramps that must stay perceivable.
750
+
751
+ A contrast pair is only needed for a different high-contrast target than the
752
+ automatic one, such as `{ apca: ['content', 'preferred'] }`.
753
+
754
+ In high-contrast variants, Glaze bypasses the normal tone window and uses the
755
+ full `[0, 100]` range. Edge tones can reach the edge; contrast floors have more
756
+ room to solve.
757
+
758
+ <PalettePreview schemes={['light', 'dark', 'lightContrast', 'darkContrast']} caption="The finished palette in all four schemes. In high contrast the borders deepen, soft text and accent fills move to their high-contrast floors, and the surfaces reach the ends of the scale." />
759
+
760
+ ### A slider through the tier
761
+
762
+ The two-tier model assumes contrast is a binary OS preference. When contrast is
763
+ instead a setting *your users* control, set
764
+ [`contrastLevel`](./reference/contrast.md#manual-contrast-level) to a number
765
+ and your **normal** palette slides along a `0–100` ramp: level `0` is the
766
+ palette you authored, `100` is its high-contrast variant, and everything
767
+ between is resolved at that level.
768
+
769
+ <PalettePreview
770
+ columns={[
771
+ { label: 'Light · level 0', scheme: 'light', contrastLevel: 0 },
772
+ { label: 'Light · level 50', scheme: 'light', contrastLevel: 50 },
773
+ { label: 'Light · level 100', scheme: 'light', contrastLevel: 100 },
774
+ ]}
775
+ caption="The finished palette's light variant at three levels. No new fields: the slider ramps toward the high-contrast values, escalated floors and pairs alike."
776
+ />
777
+
778
+ The authoring work is the same: the slider ramps toward the values the
779
+ high-contrast tier uses, so a palette tuned for high contrast needs no new
780
+ fields. Two things worth planning for:
781
+
782
+ * The slider and the tier compose. The high-contrast tier keeps resolving at
783
+ full contrast regardless of the level, so wire the level into your build or
784
+ runtime *and* keep your `prefers-contrast: more` block: a user who has raised
785
+ the slider and asked their OS for more contrast still gets the escalation on
786
+ top. The one exception is a global level of `100`
787
+ (`glaze.configure({ contrastLevel: 100 })`), where the two coincide and the
788
+ tier is dropped as a duplicate. A per-theme level of `100` still emits the
789
+ tier, with values equal to the normal ones.
790
+ * `contrastLevel: 0` is the natural rest position for a slider you ship but
791
+ default off — it reproduces the `'auto'` output bit for bit.
792
+
793
+ [Manual contrast level](./schemes.mdx#manual-contrast-level) shows the level
794
+ as a strip; [Contrast and high contrast](./reference/contrast.md#manual-contrast-level)
795
+ in the API reference has the exact interpolation rules.
796
+
797
+ ## Extend into status themes
798
+
799
+ Once the default theme is shaped, create colored siblings by replacing hue and
800
+ overriding only the root surface that should become visibly tinted:
801
+
802
+ ```ts
803
+ const TINTED_SURFACE_OVERRIDE = {
804
+ surface: { tone: 96, saturation: 0.8 },
805
+ };
806
+
807
+ const primaryTheme = defaultTheme.extend({
808
+ colors: TINTED_SURFACE_OVERRIDE,
809
+ });
810
+ const successTheme = defaultTheme.extend({
811
+ hue: SUCCESS_HUE,
812
+ colors: TINTED_SURFACE_OVERRIDE,
813
+ });
814
+ const dangerTheme = defaultTheme.extend({
815
+ hue: DANGER_HUE,
816
+ colors: TINTED_SURFACE_OVERRIDE,
817
+ });
818
+ const warningTheme = defaultTheme.extend({
819
+ hue: WARNING_HUE,
820
+ colors: TINTED_SURFACE_OVERRIDE,
821
+ });
822
+ const noteTheme = defaultTheme.extend({
823
+ hue: NOTE_HUE,
824
+ colors: TINTED_SURFACE_OVERRIDE,
825
+ });
826
+ ```
827
+
828
+ <StatusThemes caption="Each theme painted only with its own tokens. The token count shows inherit: false at work: the default theme carries the whole palette, each sibling only the inherited subset." />
829
+
830
+ The inherited accent tokens (including `accent-disabled-*`) now resolve in each
831
+ status hue. Tokens marked `inherit: false` stay default-only, so sibling themes
832
+ remain small. [`theme.extend()`](./reference/theme.md#themeextendoptions)
833
+ documents how inherited and overriding definitions merge.
834
+
835
+ <Callout kind="caution" title="A dark seed is inherited too">
836
+ `extend()` keeps the parent's `darkHue` and `darkSaturation` unless you pass
837
+ new ones. If the default theme has the dark seed from
838
+ [When dark needs its own chroma](#when-dark-needs-its-own-chroma), a status
839
+ theme that only sets `hue` turns purple in dark. Pass its own `darkHue` next
840
+ to `hue`, for example `hue: SUCCESS_HUE, darkHue: SUCCESS_HUE - 12`.
841
+ </Callout>
842
+
843
+ ## Export the palette
844
+
845
+ Compose the themes once:
846
+
847
+ ```ts
848
+ const palette = glaze.palette({
849
+ default: defaultTheme,
850
+ primary: primaryTheme,
851
+ success: successTheme,
852
+ danger: dangerTheme,
853
+ warning: warningTheme,
854
+ note: noteTheme,
855
+ });
856
+ ```
857
+
858
+ The usual export shape is default unprefixed and status themes prefixed:
859
+
860
+ ```ts
861
+ const prefix = {
862
+ default: '',
863
+ primary: 'primary-',
864
+ success: 'success-',
865
+ danger: 'danger-',
866
+ warning: 'warning-',
867
+ note: 'note-',
868
+ };
869
+
870
+ palette.tasty({ prefix });
871
+ ```
872
+
873
+ <PaletteOutput />
874
+
875
+ An explicit prefix map is the clearest choice when the palette has a neutral
876
+ `default` theme. The separate palette `primary` option serves another pattern:
877
+ it duplicates one named theme without a prefix while retaining its prefixed
878
+ tokens. Do not combine the two accidentally; choose the token namespace your
879
+ components expect.
880
+
881
+ The palette design is independent of the exporter:
882
+
883
+ ```ts
884
+ palette.tokens({ prefix }); // JavaScript maps, native oklch by default
885
+ palette.css({ prefix }); // CSS custom-property declarations
886
+ palette.dtcg({ prefix }); // one design-token tree per scheme
887
+ palette.tailwind({ prefix }); // Tailwind CSS v4 theme
888
+ ```
889
+
890
+ Use `palette.tasty({ prefix })` for [Tasty](../../tasty/README.md) state
891
+ bindings. [Choosing an export](./exporting.mdx#choosing-an-export) covers
892
+ output shapes and application wiring, and
893
+ [the `primary` alias](./exporting.mdx#the-primary-alias) the alias pattern;
894
+ every palette option is in the [Palettes](./reference/palette.md) reference.
895
+
896
+ ## Checklist
897
+
898
+ Before shipping a palette, verify:
899
+
900
+ * Text, icon, and content tokens either have APCA/WCAG contrast or are
901
+ deliberately edge-anchored.
902
+ * Accent fills use `mode: 'fixed'`; accent foregrounds on neutral UI stay
903
+ `mode: 'auto'` and are based on `surface`.
904
+ * Ambiguous APCA tokens have an explicit `role`; obvious names rely on
905
+ inference.
906
+ * Low-stakes visual relationships use tone deltas instead of fake contrast
907
+ floors.
908
+ * `inherit: false` is set on default-only tokens so status themes stay
909
+ focused.
910
+ * Tones, shadow / glow intensities, and mix values that should change in high
911
+ contrast have a high-contrast pair. Contrast floors escalate on their own;
912
+ both feed a manual `contrastLevel`.
913
+ * Status themes extended from a theme with a dark seed pass their own
914
+ `darkHue`.
915
+ * `glaze.configure({ states, modes })` matches the states registered in the
916
+ app.
917
+ * Every emitted scheme (`light`, `dark`, `lightContrast`, `darkContrast`) has
918
+ been reviewed on complete screens, not only in a token grid.
919
+ * Rendered WCAG/APCA results have been checked for chromatic foreground/base
920
+ pairs that carry accessibility requirements.
921
+ * Resolution emits no unexplained unreachable-contrast (colors and mixes),
922
+ unreachable shadow / glow, or token-collision warnings.