@tenphi/glaze 0.0.0-snapshot.c13086f → 0.0.0-snapshot.c61bbdf

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.
@@ -0,0 +1,278 @@
1
+ ---
2
+ title: Migration and upgrading
3
+ description: Move an existing color system to Glaze without renaming tokens, and upgrade between major Glaze versions.
4
+ ---
5
+
6
+ This page covers two kinds of move: bringing an existing CSS, design-token, or
7
+ application color system over to Glaze, and upgrading Glaze itself across a
8
+ major version. For designing a palette from scratch, see
9
+ [Designing a palette](./methodology.mdx); for choosing an export shape and
10
+ wiring it into your app, see [Exporting and integration](./exporting.mdx).
11
+
12
+ ## Migrating from an existing color system
13
+
14
+ The fastest path is **map first, design second**. Get every existing token name
15
+ producing a Glaze-resolved color (even a rough first pass), wire the new
16
+ palette in, then iterate on the resolved colors without touching component
17
+ code.
18
+
19
+ ### 1. Inventory the existing tokens
20
+
21
+ Walk your current color system and put every token into one of these buckets:
22
+
23
+ - **Surfaces**: backgrounds, panels, cards, banners.
24
+ - **Surface text**: body text, headings, captions, labels.
25
+ - **Borders, dividers, focus rings**: neutral structural lines.
26
+ - **Brand fills**: solid CTA backgrounds, badges, status banners.
27
+ - **Brand foregrounds**: link text, status text, icon colors on neutral
28
+ backgrounds.
29
+ - **Disabled**: disabled chip and its label.
30
+ - **Shadows and overlays**: elevation and state layers (`shadow` / `glow`),
31
+ and fixed-alpha overlays such as scrims (`opacity`).
32
+ - **One-off colors**: syntax highlighting, charts, illustrations.
33
+
34
+ Each bucket maps onto one of the patterns in
35
+ [Designing a palette](./methodology.mdx). The bucket decides the _shape_ of the
36
+ Glaze definition (root or dependent, `mode: 'auto'` or `'fixed'`, a contrast
37
+ floor or an absolute tone), not its value.
38
+
39
+ ### 2. Reproduce the existing values
40
+
41
+ Pick a definition shape that lands the new color _close to_ the legacy value in
42
+ light mode. [Designing a palette](./methodology.mdx) explains the shape per
43
+ bucket; the [Color definitions](./reference/color-definitions.md) reference
44
+ covers the levers (`tone`, `saturation`, `contrast`, `mode`, `hue`).
45
+
46
+ Three tactics make matching easier:
47
+
48
+ - **Anchor strong text at the edge** instead of solving for `'AAA'`. The
49
+ contrast solver stops at the floor, which usually leaves text noticeably
50
+ softer than the legacy value: on a white surface, a neutral
51
+ `contrast: 'AAA'` (7:1) solves to tone 36, while a legacy `#1a1a1a` sits at
52
+ tone 6.2 (17.4:1). An absolute `tone` preserves the look, but author it a
53
+ little below the measured value: the light
54
+ [tone window](./schemes.mdx#tone-windows) lifts authored tones (tone 0
55
+ renders at about 5, tone 2 at 6.9), so a neutral `tone: 1.3` renders
56
+ `#1a1a1a`. With `lightTone: false` on the theme, light tones render exactly
57
+ as authored.
58
+ - **Match the metric the old system actually used.** Numeric WCAG ratios are
59
+ useful when you reproduce a measured legacy ratio. For new content roles,
60
+ prefer the APCA presets from the methodology. For low-stakes spacing, use a
61
+ tone delta instead of inventing a contrast target. See
62
+ [Contrast solving](./contrast.mdx).
63
+ - **Pin values that are a contract with `from`.** A brand color you must honor
64
+ can be given as a literal: `{ from: '#2f5bff' }` reproduces that exact value
65
+ in the light scheme whatever the theme's seed saturation, while dark and high
66
+ contrast still adapt. Add `base` and `contrast` when it must stay legible
67
+ against something. See [`from`](./reference/color-definitions.md#from).
68
+
69
+ ### 3. Keep the old token names
70
+
71
+ Use a custom `prefix` map (and [alias themes](./exporting.mdx#alias-themes-for-legacy-names)
72
+ if needed) so the names your components already consume keep working:
73
+
74
+ ```ts
75
+ // Old: components consume `#dark`, `#dark-02`, `#dark-03`.
76
+ // New: define them in Glaze, map the prefix so they emit unchanged.
77
+
78
+ defaultTheme.colors({
79
+ dark: { base: 'surface', tone: 2, saturation: 0.475 },
80
+ 'dark-02': {
81
+ base: 'surface',
82
+ tone: '-1',
83
+ saturation: 0.375,
84
+ contrast: [9, 11],
85
+ },
86
+ 'dark-03': {
87
+ base: 'surface',
88
+ tone: '-1',
89
+ saturation: 0.24,
90
+ contrast: [4.5, 5.5],
91
+ },
92
+ });
93
+
94
+ palette.tasty({ prefix: { default: '' } });
95
+ // → '#dark', '#dark-02', '#dark-03': components don't change.
96
+ ```
97
+
98
+ Once consumers are off the legacy names, rename the Glaze tokens to match your
99
+ conventions. [Prefix map strategies](./exporting.mdx#prefix-map-strategies)
100
+ shows every naming option with real output.
101
+
102
+ ### 4. Verify dark mode and high contrast before promoting
103
+
104
+ Glaze gives you light, dark, and high contrast for free, but only the light
105
+ scheme was matched against the legacy palette. Before you promote the
106
+ migration:
107
+
108
+ - Spot-check every surface, text, accent, and disabled pair in dark mode. The
109
+ tone inversion plus the per-color `mode` can produce results that look right
110
+ in light but feel off in dark. The usual fixes: switch a brand color to
111
+ `mode: 'fixed'`, or anchor a foreground to `surface` instead of the brand
112
+ fill (see [adaptation modes](./schemes.mdx#adaptation-modes) and
113
+ [Designing a palette](./methodology.mdx)).
114
+ - If the legacy system had no high-contrast mode, audit the high-contrast
115
+ variants Glaze emits. Wherever the resolved contrast is too low or a color
116
+ blows out, add a `[normal, highContrast]` pair (`tone: ['-7', '-20']`,
117
+ `contrast: [4.5, 7]`, and so on).
118
+ - Run real screens, not just the token grid. The interaction of several Glaze
119
+ tokens against each other (text on a chip, a hover background against its
120
+ fill, a disabled label on a disabled chip) is where mismatches show up.
121
+
122
+ ### 5. Trim what `extend()` doesn't need
123
+
124
+ After the migration, mark every default-only token (borders, shadows, glows,
125
+ the disabled chip, code highlighting, and so on) `inherit: false`. Colored
126
+ sibling themes created with `extend()` only need the accent and tinted-surface
127
+ chain; flagging the rest cuts the token set each theme emits dramatically.
128
+
129
+ ## Upgrading Glaze
130
+
131
+ ### 3.0 — shadows and glows
132
+
133
+ `shadow` was rebuilt so `intensity` produces the same visible change on every
134
+ surface, and `glow` was added as its mirror. Both emit a translucent pigment
135
+ whose opacity is solved, per surface and per scheme, so the composite lands an
136
+ exact **tone step** away from `bg`: darker for `shadow`, lighter for `glow`.
137
+ See [Shadows and glows](./shadows-and-glows.mdx).
138
+
139
+ What changed:
140
+
141
+ - **`intensity` is a tone step.** In 2.x it went through a `tanh` curve in
142
+ OKHSL lightness, so the same number darkened white by 9 tones, a tone-35
143
+ surface by 13.9, and a tone-6 surface by 4.4. Now `intensity: 10` is 10 tones
144
+ on every surface with 10 tones of room. A step past black (or white, for a
145
+ glow) is pinned to the strongest composite the pigment reaches, and Glaze
146
+ warns; see [Unreachable steps](./shadows-and-glows.mdx#unreachable-steps).
147
+ - **`fg` is the object, not a contrast reference.** In 2.x, strength scaled
148
+ with `|l_bg − l_fg|` and the docs pointed `fg` at `text`, which ran opposite
149
+ to the physics: a white card on a light surface got almost no shadow. Now
150
+ `fg` is the element casting the shadow; a darker object deepens it, a lighter
151
+ one softens it, and an object at the surface's tone (or no `fg`) gives
152
+ exactly `intensity`.
153
+ - **The pigment is neutral by default**: pure black for `shadow`, pure white
154
+ for `glow`. The surface keeps its hue and the output is clean: `intensity: 10`
155
+ on a white surface emits `rgb(0 0 0 / 0.1326)`, which composites to tone 90.
156
+ - **The `ShadowTuning` fields were replaced.** `saturationFactor`,
157
+ `maxSaturation`, `lightnessFactor`, `lightnessBounds`, `minGapTarget`,
158
+ `alphaMax`, and `bgHueBlend` are gone. The new knobs are `objectInfluence`,
159
+ `tint`, `reach`, and `hueBleed`, shared by `shadowTuning` and the new
160
+ `glowTuning`. The type is now `LightingTuning`; `ShadowTuning` and
161
+ `GlowTuning` remain as aliases of it.
162
+ - **`GLAZE_EXPORT_VERSION` is 2.** A version-1 theme or palette snapshot that
163
+ contains shadow colors or a `shadowTuning` now throws on restore instead of
164
+ silently re-rendering every shadow.
165
+ - **`ColorDef` has a fourth member, `GlowColorDef`.** Exhaustive `switch`es
166
+ over `def.type` need a `'glow'` branch.
167
+
168
+ How 2.x shadows map onto 3.0 (2.x steps measured with browser compositing):
169
+
170
+ | 2.x definition | 2.x step for `intensity: 10` | 3.0 equivalent |
171
+ | --- | --- | --- |
172
+ | No `fg`, light surface (tone 97–100) | ≈ 9 tones | `intensity: 9`, or keep `10` (a tone stronger) |
173
+ | No `fg`, mid surface (tone 50) | ≈ 13.6 tones | `intensity: 13.6`, or settle on one value for all surfaces |
174
+ | No `fg`, dark surface (tone 10) | ≈ 7.6 tones, and saturating | `intensity: 7.6`; dark shadows were weaker than authored and now aren't |
175
+ | `fg: 'text'` on a light surface | ≈ 7 tones | Drop `fg` and use `intensity: 7`, or point `fg` at the element and re-tune |
176
+ | `fg` = a near-white card on light | ≈ 0.3 tones (barely visible) | `fg: 'card'`, then pick `intensity` for the look you want |
177
+
178
+ Then:
179
+
180
+ 1. Replace `fg: 'text'` (or any contrast-reference `fg`) with either no `fg` or
181
+ the element that casts the shadow.
182
+ 2. Pick `intensity` as the number of tones you want. For a ladder, `4` / `8` /
183
+ `12` reads as evenly spaced on every surface with room for the deepest step.
184
+ Check the dark schemes: a white `auto` page sits at about tone 9 in dark and
185
+ at tone 0 in dark high contrast. Shadows on it deeper than about `8`–`9` (a
186
+ saturated page has a little less room) pin to black and warn in dark, and
187
+ every shadow on it stays transparent and warns in dark high contrast. Use
188
+ glows for dark elevation, on a surface that also leaves them room in light
189
+ ([Dark-mode elevation](./shadows-and-glows.mdx#dark-mode-elevation)). Leaving
190
+ a shadow token out of the dark schemes hides the pinned result but not the
191
+ warning: every color resolves in all four schemes.
192
+ 3. Delete the old fields inside `tuning` / `shadowTuning` (`saturationFactor`,
193
+ `alphaMax`, …); TypeScript flags them. A tinted pigment is
194
+ `tuning: { tint: 0.3, reach: 0.85 }`. There is no opacity cap: lower
195
+ `intensity` for a lighter overlay.
196
+ 4. Replace hand-tuned translucent white mixes (dark-mode elevation, edge
197
+ highlights, lightening state layers) with
198
+ `{ type: 'glow', bg, intensity }`. Direction is fixed per type, so a
199
+ `shadow` darkens and a `glow` lightens in light and dark alike. A glow on a
200
+ white light-mode surface has nothing to lighten and warns.
201
+ 5. Re-export snapshots. A 2.x snapshot with shadows or a `shadowTuning` throws
202
+ on restore, so re-author it. In code, rebuild the theme with 3.0 shadow defs
203
+ and call `export()`. In the JSON, fix the shadow defs, drop
204
+ `config.shadowTuning`, and set `version` to `2` (a palette snapshot has one at
205
+ the top and one per theme), then load it and export again. Snapshots without
206
+ shadows load unchanged.
207
+
208
+ `glaze.shadow()` keeps its signature with the new semantics, and `glaze.glow()`
209
+ takes the same input. See
210
+ [`glaze.shadow()` / `glaze.glow()`](./reference/utilities.md#glazeshadowinput--glazeglowinput).
211
+
212
+ ### 2.0 — `format*` takes 0–1
213
+
214
+ The `format*` writers (`formatOkhsl`, `formatOkhst`, `formatRgb`, `formatHsl`,
215
+ `formatOklch`) used to take `s` / `l` / `t` on a 0–100 percentage scale, while
216
+ everything that _produces_ those values (`resolve()`, `variantToOkhsl`,
217
+ `srgbToOkhsl`, `oklabToOkhsl`, `okhslToSrgb`) returns them on 0–1. Composing
218
+ the two, which is the obvious thing to do, was off by 100x and failed
219
+ silently: `0.7` is a legal percentage, so you got a valid CSS string naming the
220
+ wrong (near-black) color.
221
+
222
+ The writers now take the 0–1 factors, so the whole library speaks one scale.
223
+ Drop the `* 100`:
224
+
225
+ ```diff
226
+ - formatOkhsl(v.h, v.s * 100, l * 100);
227
+ + formatOkhsl(v.h, v.s, l);
228
+
229
+ - formatOkhst(v.h, v.s * 100, v.t * 100);
230
+ + formatOkhst(v.h, v.s, v.t);
231
+ ```
232
+
233
+ Nothing else moves: `h` was always 0–360, `alpha` was always 0–1, and every
234
+ export method (`css()`, `tokens()`, `json()`, `tasty()`, `dtcg()`,
235
+ `tailwind()`, `glaze.format()`) emits the same colors, because they were
236
+ compensating internally. Dropping the redundant `×100 ÷100` round-trip shifts
237
+ float noise by an ULP, which shows up nowhere except the (meaningless) hue term
238
+ of a fully desaturated `hsl()` string. `toTone` / `fromTone` still speak the
239
+ 0–100 tone axis the authoring API takes, so divide a tone from them by 100
240
+ before passing it to `formatOkhst`.
241
+
242
+ A leftover `* 100` is no longer silent: a value above 1 cannot be a factor, so
243
+ the writers `console.warn` once per writer, and you get an obviously broken
244
+ string such as `okhsl(280 7040.68% 5000%)` instead of a plausible wrong color.
245
+ See [format writers](./reference/utilities.md#format-writers).
246
+
247
+ ### 0.14 — `lightness` became `tone`
248
+
249
+ Colors used to be authored on OKHSL lightness. They are now authored on
250
+ **tone**, a contrast-uniform 0–100 axis (see [Tone and OKHST](./okhst.mdx)):
251
+
252
+ - The `lightness` prop is gone everywhere (theme colors, `glaze.color()`
253
+ structured input, tone deltas); use `tone`. Equal tone steps give equal
254
+ WCAG contrast, so numeric values don't map to the same OKHSL lightness as
255
+ before. Re-check absolute mid-range values.
256
+ - The config windows `lightLightness` / `darkLightness` became `lightTone` /
257
+ `darkTone`, and `darkCurve` was removed.
258
+ - `ResolvedColorVariant` stores `{ h, s, t, alpha }` instead of `{ h, s, l }`.
259
+ Use `variantToOkhsl()` to recover OKHSL lightness.
260
+ - Tone deltas that overshoot `[0, 100]` mirror to the other side of the base
261
+ by default instead of clamping. Set `autoFlip: false` on the color
262
+ (or globally) to clamp.
263
+
264
+ ## Common pitfalls
265
+
266
+ | Symptom | Cause | Fix |
267
+ | --- | --- | --- |
268
+ | A disabled state stops looking disabled in dark mode. | An alpha-tinted overlay on `surface-text` (which inverts) gives asymmetric perceived contrast. | Replace it with an adaptive color anchored to `surface`: a tone delta for visual spacing, or a contrast floor when one is required. See [chips and disabled states](./methodology.mdx#chips-and-disabled-states). |
269
+ | A brand fill turns dark in dark mode and stops reading as the brand. | The default `mode: 'auto'` inverts the tone. | Set `mode: 'fixed'` so the tone is remapped into the dark window, not inverted. See [adaptation modes](./schemes.mdx#adaptation-modes). |
270
+ | Brand text washes out against the dark surface. | The foreground was anchored to `accent-surface` (the brand fill), so contrast was enforced only against that fill, not against the surface it sits on. | Anchor `accent-text` and friends to `surface` with `mode: 'auto'`. |
271
+ | Tokens look right in light but broken in high contrast. | High contrast bypasses the tone window, so absolute tones and deltas land on the full `[0, 100]` range (a white `auto` page is black in dark high contrast), and floors without an explicit high-contrast value rise a tier (`'AA'` → `'AAA'`, APCA +15 Lc). | Add explicit `[normal, highContrast]` pairs to `tone` / `contrast` for the affected tokens. |
272
+ | A tone delta like `'+48'` lands on the _wrong_ (darker) side of its base. | Overshooting tone deltas mirror to the other side of the base by default (a color's `autoFlip` defaults to the global `autoFlip`, which is on). | Set `autoFlip: false` on the color to clamp to the boundary instead, or use `tone: 'max'` / `'min'` to force the extreme. |
273
+ | `palette.tokens()` emits unexpected unprefixed names. | A `primary` was set on the palette (or per call) and duplicates that theme's tokens without a prefix. | Pass `primary: false` to disable it for that export, or change `glaze.palette(themes, { primary })`. See [the `primary` alias](./exporting.mdx#the-primary-alias). |
274
+ | `glaze: token "surface" from theme "danger" collides with theme "default" — skipping.` | Two themes resolved to the same output name under your prefix config. | Adjust the prefix map so each name is unique, or accept first-write-wins. See [Collisions](./exporting.mdx#collisions). |
275
+ | `glaze: formatOkhsl() got a value above 1 …` | Pre-2.0 percentage-scale input to a `format*` writer (a leftover `* 100`). | Pass the 0–1 factors the converters return; see [2.0 — `format*` takes 0–1](#20--format-takes-01). |
276
+ | `glaze: color "X" cannot meet WCAG … in light scheme (got …)` | The contrast target is physically unreachable for the color's hue and saturation against its base. The color pins to the extreme of the side it searched first, which may not be the closer one; see [When a floor can't be met](./contrast.mdx#when-a-floor-cant-be-met). | Lower the floor or change the base. If only the high-contrast schemes fail, give `contrast` an explicit, reachable high-contrast entry (`['AA', 'AA']`). On standalone colors, set `name` so the warning is identifiable; see [`name`](./reference/color-tokens.md#name). |
277
+ | `glaze: shadow "X" cannot reach a 10.0-tone step …` (or `glow`) | The surface has fewer tones of room than `intensity` asks for: a glow on white, a deep shadow on near-black, or any shadow on a light `auto` page in dark high contrast (at or near tone 0 there). The overlay is pinned to its strongest composite (transparent when the surface is already at the pigment's extreme). | Lower `intensity` or give the overlay a surface with room in every scheme, and use glows for dark elevation. Otherwise expect the warning: every color resolves in all four schemes, so not using the token in a scheme doesn't silence it. See [Unreachable steps](./shadows-and-glows.mdx#unreachable-steps). |
278
+ | `glaze.themeFrom: this snapshot is export version 1 (Glaze 2) …` | A 2.x snapshot with shadow colors or `shadowTuning`; shadows changed meaning in 3.0. | Re-author the shadows and export again; see [3.0 — shadows and glows](#30--shadows-and-glows). |
package/docs/mixes.mdx ADDED
@@ -0,0 +1,268 @@
1
+ ---
2
+ title: Mixes and transparency
3
+ description: Blend two colors into a solid tint or a translucent wash, optionally solved for contrast.
4
+ ---
5
+
6
+ import Example from '../docs-site/components/Example.astro';
7
+ import MixRamp from '../docs-site/components/mixes/MixRamp.tsx';
8
+ import TransparentMix from '../docs-site/components/mixes/TransparentMix.tsx';
9
+ import ContrastMix from '../docs-site/components/mixes/ContrastMix.tsx';
10
+
11
+ export const washTheme = {
12
+ hue: 280,
13
+ saturation: 80,
14
+ colors: {
15
+ surface: { tone: 97 },
16
+ accent: { tone: 45 },
17
+ 'wash-sm': { type: 'mix', base: 'surface', target: 'accent', value: 6, blend: 'transparent' },
18
+ 'wash-md': { type: 'mix', base: 'surface', target: 'accent', value: 15, blend: 'transparent' },
19
+ 'wash-lg': { type: 'mix', base: 'surface', target: 'accent', value: 30, blend: 'transparent' },
20
+ },
21
+ };
22
+
23
+ export const contrastTheme = {
24
+ hue: 280,
25
+ saturation: 80,
26
+ colors: {
27
+ surface: { tone: 95 },
28
+ accent: { tone: 30 },
29
+ 'accent-border': { type: 'mix', base: 'surface', target: 'accent', value: 20, contrast: 3 },
30
+ 'accent-wash': { type: 'mix', base: 'surface', target: 'accent', value: 4, blend: 'transparent', contrast: 1.25 },
31
+ },
32
+ };
33
+
34
+ A mix color blends a `base` color toward a `target` color by `value`, from
35
+ `0` (pure base) to `100` (pure target). An **opaque** mix produces a solid
36
+ color; a **transparent** mix produces the target with an opacity, for the
37
+ browser to composite over the base. Both read `base` and `target` per scheme,
38
+ so they adapt to light, dark, and high contrast automatically.
39
+
40
+ ```ts
41
+ import { glaze } from '@tenphi/glaze';
42
+
43
+ const theme = glaze(280, 80);
44
+
45
+ theme.colors({
46
+ surface: { tone: 95 },
47
+ accent: { tone: 30 },
48
+
49
+ // A solid tint: 30% of the way from surface to accent.
50
+ 'accent-tint': {
51
+ type: 'mix',
52
+ base: 'surface',
53
+ target: 'accent',
54
+ value: 30,
55
+ },
56
+
57
+ // A translucent wash: accent at 8% opacity, composited over surface.
58
+ 'accent-hover': {
59
+ type: 'mix',
60
+ base: 'surface',
61
+ target: 'accent',
62
+ value: 8,
63
+ blend: 'transparent',
64
+ },
65
+ });
66
+ ```
67
+
68
+ Every field is listed in [Color definitions](./reference/color-definitions.md#mixcolordef).
69
+
70
+ ## Opaque mix
71
+
72
+ An opaque mix (the default, `blend: 'opaque'`) interpolates between the
73
+ resolved base and target and emits a solid color with alpha 1. `value: 0` is
74
+ the base, `value: 100` is the target. Because base and target are resolved for
75
+ each scheme first, the mix follows them: a tint of the accent over the surface
76
+ in light mode is a tint of the dark-mode accent over the dark-mode surface in
77
+ dark mode.
78
+
79
+ ### Blend space
80
+
81
+ `space` picks where an opaque mix interpolates:
82
+
83
+ | `space` | Behavior | Best for |
84
+ | --- | --- | --- |
85
+ | `'okhsl'` (default) | Perceptually uniform OKHSL interpolation: hue, saturation, and lightness each move evenly. | Design token derivation. |
86
+ | `'srgb'` | Linear-light sRGB channel interpolation, like CSS `color-mix(in srgb-linear, …)`. | Physical light mixing. |
87
+
88
+ The same two colors, mixed at every tenth of the way in each space:
89
+
90
+ <MixRamp
91
+ hue={260}
92
+ saturation={90}
93
+ colors={{
94
+ blue: { tone: 40, mode: 'static' },
95
+ yellow: { tone: 90, hue: 95, mode: 'static' },
96
+ }}
97
+ base="blue"
98
+ target="yellow"
99
+ caption="Blue (hue 260, tone 40) toward yellow (hue 95, tone 90). Top number: value. Bottom number: the result's tone. OKHSL walks the hue around the wheel through cyan and green; linear sRGB passes through desaturated colors."
100
+ />
101
+
102
+ <MixRamp
103
+ hue={0}
104
+ saturation={0}
105
+ colors={{
106
+ black: { tone: 0, mode: 'static' },
107
+ white: { tone: 100, mode: 'static' },
108
+ }}
109
+ base="black"
110
+ target="white"
111
+ caption="Black toward white. OKHSL spaces lightness evenly; linear light gets bright quickly, so most of the ramp sits at high tones."
112
+ />
113
+
114
+ ### Achromatic colors
115
+
116
+ When one side of an `okhsl` mix has a saturation of zero (a color defined with
117
+ `saturation: 0`, such as a pure white, black, or gray), its hue carries no
118
+ information, so the mix takes the hue of the color that has saturation. This
119
+ matches the "missing component" behavior of CSS `color-mix()`: a mix from
120
+ such a white toward an accent stays in the accent's hue the whole way instead
121
+ of sweeping through other hues.
122
+
123
+ Only exactly zero counts. A white or black that keeps any saturation, such as
124
+ `{ tone: 100 }` in a saturated theme, still carries its hue, and a mix toward
125
+ an accent of another hue sweeps between the two. A mix between two
126
+ zero-saturation colors stays gray in either space; the spaces differ only in
127
+ how lightness is spread along the ramp (see black toward white above).
128
+
129
+ <MixRamp
130
+ hue={280}
131
+ saturation={80}
132
+ colors={{
133
+ white: { tone: 100, saturation: 0, mode: 'static' },
134
+ accent: { tone: 40, mode: 'static' },
135
+ }}
136
+ base="white"
137
+ target="accent"
138
+ spaces={['okhsl']}
139
+ caption="White toward a hue-280 accent in OKHSL. White has no hue, so every step uses the accent's hue while saturation and lightness move."
140
+ />
141
+
142
+ ## Transparent mix
143
+
144
+ With `blend: 'transparent'`, the output is the **target** color (its resolved
145
+ hue, saturation, and tone) with `alpha = value / 100`. Nothing is blended in
146
+ Glaze; the browser composites it over whatever is underneath, which should be
147
+ the `base`.
148
+
149
+ ```ts
150
+ theme.colors({
151
+ surface: { tone: 95 },
152
+ black: { tone: 0, saturation: 0 },
153
+ hover: {
154
+ type: 'mix',
155
+ base: 'surface',
156
+ target: 'black',
157
+ value: 8,
158
+ blend: 'transparent',
159
+ },
160
+ });
161
+ // hover → the resolved `black` with alpha 0.08
162
+ ```
163
+
164
+ The target is resolved like any color first. Here `black` uses the default
165
+ `mode: 'auto'`, so it inverts to a near-white in dark schemes and the same
166
+ `hover` lightens the dark surface instead of darkening it.
167
+
168
+ `space` is ignored for transparent mixes. Browsers composite `source-over` on
169
+ gamma-encoded sRGB: 50% black over white paints `#808080`, not the `#bcbcbc`
170
+ linear light would give. Glaze's contrast solver for transparent mixes models
171
+ exactly that, so a solved opacity renders at the contrast it was solved for.
172
+
173
+ <Example {...washTheme} visual="none" />
174
+
175
+ <TransparentMix
176
+ {...washTheme}
177
+ base="surface"
178
+ show={['wash-sm', 'wash-md', 'wash-lg']}
179
+ caption="Accent washes over the surface in your current scheme. The big number is the tone step the composite lands; the opacity is value / 100 in every scheme, so the step changes when the surface does."
180
+ />
181
+
182
+ A transparent mix keeps the opacity you author. That makes it the right tool
183
+ for a brand-colored wash, but it does not land the same visible step on every
184
+ surface. For an overlay that does, use a
185
+ [`shadow` or `glow`](./shadows-and-glows.mdx), whose opacity is solved per
186
+ surface.
187
+
188
+ ## Contrast on mixes
189
+
190
+ Mixes take the same `contrast` field as regular colors, with a WCAG ratio or
191
+ preset, or an APCA target (see [Contrast solving](./contrast.mdx)). The floor
192
+ is measured between the mix and its `base`. The solver keeps your `value` when
193
+ it already meets the floor; otherwise it moves the mix ratio (opaque) or the
194
+ opacity (transparent) toward the target until it does, staying as close to
195
+ your `value` as it can.
196
+
197
+ <Example {...contrastTheme} visual="none" />
198
+
199
+ <ContrastMix
200
+ {...contrastTheme}
201
+ show={['accent-border', 'accent-wash']}
202
+ caption="Each mix as authored (no floor) and with its floor, over the surface in your current scheme. The big number is the WCAG ratio against the surface; below it, the value or opacity the mix ended up with."
203
+ />
204
+
205
+ - Both `value` and `contrast` accept `[normal, highContrast]` pairs, and
206
+ `value` follows a manual contrast level like other pairs. Contrast presets
207
+ escalate in high contrast as they do for regular colors
208
+ ([high contrast](./contrast.mdx#high-contrast)).
209
+ - For APCA floors, `role` sets the polarity of the mix against its `base`,
210
+ exactly as on a regular color ([roles](./contrast.mdx#roles-and-apca-polarity)).
211
+ - When even the pure target cannot meet the floor, the mix settles at the
212
+ pure target for any `value` below 100. A `value` of 100 (including a
213
+ high-contrast entry of 100) has no room above, so it falls back to
214
+ `value: 0`: the base itself, and a transparent mix vanishes. Keep `value`
215
+ below 100 when you set a floor. Unlike a regular color, a mix does not warn
216
+ when its floor is unmet.
217
+
218
+ ## Mix chaining
219
+
220
+ A mix can use another mix as its `base` or `target`:
221
+
222
+ <Example
223
+ hue={0}
224
+ saturation={0}
225
+ colors={{
226
+ white: { tone: 100, saturation: 0 },
227
+ black: { tone: 0, saturation: 0 },
228
+ gray: {
229
+ type: 'mix',
230
+ base: 'white',
231
+ target: 'black',
232
+ value: 50,
233
+ space: 'srgb',
234
+ },
235
+ 'gray-light': {
236
+ type: 'mix',
237
+ base: 'white',
238
+ target: 'gray',
239
+ value: 50,
240
+ space: 'srgb',
241
+ },
242
+ }}
243
+ scheme="light"
244
+ caption="Light scheme, where the auto black sits at the light tone window's floor rather than tone 0. gray is halfway from white to black in linear sRGB; gray-light is halfway from white to gray. In dark schemes white and black invert, so the whole chain mirrors."
245
+ />
246
+
247
+ A mix **cannot** reference a `shadow` or `glow` color, as `base` or as
248
+ `target`; Glaze throws when the theme resolves. The same restriction applies to
249
+ a regular color's `base`.
250
+
251
+ Other translucent colors are accepted but lose their alpha. A transparent mix
252
+ or a color with `opacity`, used as a mix's `base` or `target` (or as a shadow's
253
+ `bg`), counts as its opaque color: a mix toward a 10% accent wash comes out the
254
+ same as a mix toward the accent. Chain off opaque colors.
255
+
256
+ ## Mix, shadow, glow, or opacity?
257
+
258
+ | You want | Use |
259
+ | --- | --- |
260
+ | The same visible darkening or lightening on any surface: state layers, elevation, drop shadows | [`shadow` / `glow`](./shadows-and-glows.mdx) |
261
+ | A solid color part of the way from one color to another | Opaque mix |
262
+ | A translucent color over a known base, such as a brand-colored wash | Transparent mix |
263
+ | A translucent color with a contrast floor against its base | Transparent mix with `contrast` |
264
+ | One fixed alpha everywhere, such as a 50% scrim | A regular color with [`opacity`](./shadows-and-glows.mdx#fixed-opacity) |
265
+
266
+ Shadows and glows solve opacity for a tone step, transparent mixes keep (or
267
+ solve, with `contrast`) an opacity against their base, and `opacity` is never
268
+ solved.