@tenphi/glaze 2.0.2 → 3.0.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/mixes.mdx ADDED
@@ -0,0 +1,348 @@
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 zero counts, along with float noise up to 1e-6. A gray given as a value,
124
+ such as `'#f5f5f5'` or `rgb(250 250 250)`, parses with a saturation of zero,
125
+ and so does an `srgb` mix that lands on a gray. A white or black that keeps
126
+ real saturation, such as `{ tone: 100 }` in a saturated theme, still carries
127
+ its hue, and a mix toward
128
+ an accent of another hue sweeps between the two. A mix between two
129
+ zero-saturation colors stays gray in either space; the spaces differ only in
130
+ how lightness is spread along the ramp (see black toward white above).
131
+
132
+ <MixRamp
133
+ hue={280}
134
+ saturation={80}
135
+ colors={{
136
+ white: { tone: 100, saturation: 0, mode: 'static' },
137
+ accent: { tone: 40, mode: 'static' },
138
+ }}
139
+ base="white"
140
+ target="accent"
141
+ spaces={['okhsl']}
142
+ 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."
143
+ />
144
+
145
+ ## Transparent mix
146
+
147
+ With `blend: 'transparent'`, the output is the **target** color (its resolved
148
+ hue, saturation, and tone, in the mix's gamut when their
149
+ [`pastel` flags](#pastel-mixes) differ) with `alpha = value / 100`. Nothing is
150
+ blended in Glaze; the browser composites it over whatever is underneath, which
151
+ should be the `base`.
152
+
153
+ ```ts
154
+ theme.colors({
155
+ surface: { tone: 95 },
156
+ black: { tone: 0, saturation: 0 },
157
+ hover: {
158
+ type: 'mix',
159
+ base: 'surface',
160
+ target: 'black',
161
+ value: 8,
162
+ blend: 'transparent',
163
+ },
164
+ });
165
+ // hover → the resolved `black` with alpha 0.08
166
+ ```
167
+
168
+ The target is resolved like any color first. Here `black` uses the default
169
+ `mode: 'auto'`, so it inverts to a near-white in dark schemes and the same
170
+ `hover` lightens the dark surface instead of darkening it.
171
+
172
+ `space` is ignored for transparent mixes. Browsers composite `source-over` on
173
+ gamma-encoded sRGB: 50% black over white paints `#808080`, not the `#bcbcbc`
174
+ linear light would give. Glaze's contrast solver for transparent mixes models
175
+ exactly that, so a solved opacity renders at the contrast it was solved for.
176
+
177
+ <Example {...washTheme} visual="none" />
178
+
179
+ <TransparentMix
180
+ {...washTheme}
181
+ base="surface"
182
+ show={['wash-sm', 'wash-md', 'wash-lg']}
183
+ 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."
184
+ />
185
+
186
+ A transparent mix keeps the opacity you author. That makes it the right tool
187
+ for a brand-colored wash, but it does not land the same visible step on every
188
+ surface. For an overlay that does, use a
189
+ [`shadow` or `glow`](./shadows-and-glows.mdx), whose opacity is solved per
190
+ surface.
191
+
192
+ ## Contrast on mixes
193
+
194
+ Mixes take the same `contrast` field as regular colors, with a WCAG ratio or
195
+ preset, or an APCA target (see [Contrast solving](./contrast.mdx)). The floor
196
+ is measured between the mix and its `base`. The solver keeps your `value` when
197
+ it already meets the floor; otherwise it moves the mix ratio (opaque) or the
198
+ opacity (transparent) toward the target until it does, staying as close to
199
+ your `value` as it can.
200
+
201
+ <Example {...contrastTheme} visual="none" />
202
+
203
+ <ContrastMix
204
+ {...contrastTheme}
205
+ show={['accent-border', 'accent-wash']}
206
+ 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."
207
+ />
208
+
209
+ - Both `value` and `contrast` accept `[normal, highContrast]` pairs, and
210
+ `value` follows a manual contrast level like other pairs. Contrast presets
211
+ escalate in high contrast as they do for regular colors
212
+ ([high contrast](./contrast.mdx#high-contrast)).
213
+ - For APCA floors, `role` sets the polarity of the mix against its `base`,
214
+ exactly as on a regular color ([roles](./contrast.mdx#roles-and-apca-polarity)).
215
+ - When even the pure target cannot meet the floor, the mix settles at the
216
+ pure target for any `value` below 100.
217
+ - A `value` of 100 is the pure target already. Glaze keeps it whenever it
218
+ clears the floor, even inside the solver's safety margin. When it misses,
219
+ there is no room above, so the mix falls back to `value: 0`: the base
220
+ itself, and a transparent mix vanishes. That includes a high-contrast entry
221
+ of 100. Keep `value` below 100 when you set a floor.
222
+
223
+ An unmet floor warns once per scheme, like it does for a regular color. Below,
224
+ `accent-fill` meets AA in light at about 4.55:1. In dark the pure target
225
+ reaches only 4.24:1, so the mix settles there. High contrast escalates the
226
+ floor to 7:1, which the pure target misses (5.72:1 in light high contrast,
227
+ 5.20:1 in dark), so the high-contrast entry of 100 falls back to the surface:
228
+
229
+ ```ts
230
+ const theme = glaze(280, 80);
231
+
232
+ theme.colors({
233
+ surface: { tone: 95 },
234
+ accent: { tone: 40 },
235
+ 'accent-fill': {
236
+ type: 'mix',
237
+ base: 'surface',
238
+ target: 'accent',
239
+ value: [80, 100],
240
+ contrast: 'AA',
241
+ },
242
+ });
243
+ ```
244
+
245
+ ```text
246
+ glaze: mix "accent-fill" cannot meet WCAG 7.00 against its base in
247
+ lightContrast scheme (got 1.00).
248
+ Even the pure target falls short; try lowering the contrast target or
249
+ mixing toward a color further from the base.
250
+ ```
251
+
252
+ The `darkContrast` warning is the same with its scheme name, and the
253
+ dark-scheme one names `WCAG 4.50` and `(got 4.24)` instead. The fix is a
254
+ target further from the base or a lower floor. Here
255
+ `contrast: ['AA', 'AA']` holds high contrast at 4.5:1, which the pure target
256
+ clears, so its entry of 100 is kept. See
257
+ [Warnings](./reference/configuration.md#mix-contrast-unmet).
258
+
259
+ ## Mix chaining
260
+
261
+ A mix can use another mix as its `base` or `target`:
262
+
263
+ <Example
264
+ hue={0}
265
+ saturation={0}
266
+ colors={{
267
+ white: { tone: 100, saturation: 0 },
268
+ black: { tone: 0, saturation: 0 },
269
+ gray: {
270
+ type: 'mix',
271
+ base: 'white',
272
+ target: 'black',
273
+ value: 50,
274
+ space: 'srgb',
275
+ },
276
+ 'gray-light': {
277
+ type: 'mix',
278
+ base: 'white',
279
+ target: 'gray',
280
+ value: 50,
281
+ space: 'srgb',
282
+ },
283
+ }}
284
+ scheme="light"
285
+ 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."
286
+ />
287
+
288
+ A mix **cannot** reference a `shadow` or `glow` color, as `base` or as
289
+ `target`; Glaze throws when the theme resolves. The same restriction applies to
290
+ a regular color's `base`.
291
+
292
+ Other translucent colors are accepted but lose their alpha. A transparent mix
293
+ or a color with `opacity`, used as a mix's `base` or `target` (or as a shadow's
294
+ `bg`), counts as its opaque color: a mix toward a 10% accent wash comes out the
295
+ same as a mix toward the accent. Chain off opaque colors.
296
+
297
+ ## Pastel mixes
298
+
299
+ A mix's `pastel` sets the gamut of the color it emits. Like any color, a mix
300
+ takes the theme's `pastel` unless it sets its own. Its base and target keep
301
+ their own flags, and when a flag differs from the mix's, the mix reads that
302
+ color in its own gamut through the color it paints:
303
+
304
+ - A standard mix holds every pastel color, so `value: 0` paints the base and
305
+ `value: 100` the target exactly.
306
+ - A pastel mix caps a vivid color at the pastel limit, keeping its hue and
307
+ lightness. A pastel mix toward a vivid brand color ends at the brand with
308
+ its saturation capped.
309
+ - A transparent mix's overlay is the target in the mix's gamut, and an
310
+ `srgb` mix interpolates the colors as painted, then caps the result.
311
+
312
+ ```ts
313
+ const theme = glaze(280, 80, { pastel: true });
314
+
315
+ theme.colors({
316
+ surface: { tone: 95 },
317
+ brand: { tone: 40, pastel: false },
318
+ // pastel like the theme: at value 100 it would be the brand, capped
319
+ 'brand-soft': { type: 'mix', base: 'surface', target: 'brand', value: 60 },
320
+ // standard: the brand in full at value 100
321
+ 'brand-tint': {
322
+ type: 'mix',
323
+ base: 'surface',
324
+ target: 'brand',
325
+ value: 60,
326
+ pastel: false,
327
+ },
328
+ });
329
+ ```
330
+
331
+ The contrast solver measures the color the mix paints, so a floor holds on
332
+ screen. When a pastel mix misses its floor because the cap took the target's
333
+ saturation, the [warning](./reference/configuration.md#mix-contrast-unmet)
334
+ says so; `pastel: false` on the mix paints the target in full.
335
+
336
+ ## Mix, shadow, glow, or opacity?
337
+
338
+ | You want | Use |
339
+ | --- | --- |
340
+ | The same visible darkening or lightening on any surface: state layers, elevation, drop shadows | [`shadow` / `glow`](./shadows-and-glows.mdx) |
341
+ | A solid color part of the way from one color to another | Opaque mix |
342
+ | A translucent color over a known base, such as a brand-colored wash | Transparent mix |
343
+ | A translucent color with a contrast floor against its base | Transparent mix with `contrast` |
344
+ | One fixed alpha everywhere, such as a 50% scrim | A regular color with [`opacity`](./shadows-and-glows.mdx#fixed-opacity) |
345
+
346
+ Shadows and glows solve opacity for a tone step, transparent mixes keep (or
347
+ solve, with `contrast`) an opacity against their base, and `opacity` is never
348
+ solved.