@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.
- package/docs/customization-rules.md +7 -5
- package/docs/migration.md +52 -26
- package/docs/theme-and-components.md +32 -15
- package/docs/upstream/glaze/README.md +46 -26
- package/docs/upstream/glaze/docs/contrast.mdx +751 -0
- package/docs/upstream/glaze/docs/exporting.mdx +634 -0
- package/docs/upstream/glaze/docs/getting-started.mdx +489 -0
- package/docs/upstream/glaze/docs/hdr.mdx +523 -0
- package/docs/upstream/glaze/docs/methodology.mdx +922 -0
- package/docs/upstream/glaze/docs/migration.md +477 -286
- package/docs/upstream/glaze/docs/mixes.mdx +348 -0
- package/docs/upstream/glaze/docs/okhst.mdx +679 -0
- package/docs/upstream/glaze/docs/reference/color-definitions.md +552 -0
- package/docs/upstream/glaze/docs/reference/color-tokens.md +420 -0
- package/docs/upstream/glaze/docs/reference/configuration.md +527 -0
- package/docs/upstream/glaze/docs/reference/contrast.md +277 -0
- package/docs/upstream/glaze/docs/reference/index.md +225 -0
- package/docs/upstream/glaze/docs/reference/output.md +713 -0
- package/docs/upstream/glaze/docs/reference/palette.md +434 -0
- package/docs/upstream/glaze/docs/reference/theme.md +331 -0
- package/docs/upstream/glaze/docs/reference/utilities.md +646 -0
- package/docs/upstream/glaze/docs/schemes.mdx +635 -0
- package/docs/upstream/glaze/docs/shadows-and-glows.mdx +519 -0
- package/docs/upstream/manifest.json +2 -2
- package/package.json +3 -3
- package/docs/upstream/glaze/docs/api.md +0 -2021
- package/docs/upstream/glaze/docs/methodology.md +0 -686
- 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.
|