@tenphi/glaze 2.0.1 → 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/AGENTS.md +69 -32
- package/README.md +47 -26
- package/dist/index.cjs +3091 -1318
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +564 -119
- package/dist/index.d.mts +564 -119
- package/dist/index.mjs +3087 -1319
- package/dist/index.mjs.map +1 -1
- package/docs/contrast.mdx +751 -0
- package/docs/exporting.mdx +635 -0
- package/docs/getting-started.mdx +487 -0
- package/docs/hdr.mdx +523 -0
- package/docs/methodology.mdx +976 -0
- package/docs/migration.md +476 -285
- package/docs/mixes.mdx +348 -0
- package/docs/okhst.mdx +692 -0
- package/docs/reference/color-definitions.md +552 -0
- package/docs/reference/color-tokens.md +421 -0
- package/docs/reference/configuration.md +527 -0
- package/docs/reference/contrast.md +277 -0
- package/docs/reference/index.md +225 -0
- package/docs/reference/output.md +714 -0
- package/docs/reference/palette.md +435 -0
- package/docs/reference/theme.md +332 -0
- package/docs/reference/utilities.md +646 -0
- package/docs/schemes.mdx +636 -0
- package/docs/shadows-and-glows.mdx +519 -0
- package/package.json +10 -5
- package/docs/api.md +0 -2021
- package/docs/methodology.md +0 -686
- package/docs/okhst.md +0 -207
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Color definitions
|
|
3
|
+
description: The ColorDef union and every field of regular, shadow, glow, and mix colors.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`theme.colors(defs)` takes a `ColorMap`: color names mapped to definitions. This page lists every definition shape and field. For why the fields behave as they do, follow the links to the concept pages: [Tone and OKHST](../okhst.mdx), [Light, dark, and high contrast](../schemes.mdx), [Contrast solving](../contrast.mdx), [Shadows and glows](../shadows-and-glows.mdx), and [Mixes and transparency](../mixes.mdx).
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import type {
|
|
10
|
+
ColorDef,
|
|
11
|
+
ColorMap,
|
|
12
|
+
RegularColorDef,
|
|
13
|
+
ShadowColorDef,
|
|
14
|
+
GlowColorDef,
|
|
15
|
+
MixColorDef,
|
|
16
|
+
} from '@tenphi/glaze';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## `ColorDef`
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
type ColorDef =
|
|
23
|
+
| RegularColorDef
|
|
24
|
+
| ShadowColorDef
|
|
25
|
+
| GlowColorDef
|
|
26
|
+
| MixColorDef;
|
|
27
|
+
type ColorMap = Record<string, ColorDef>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The `type` field picks the shape:
|
|
31
|
+
|
|
32
|
+
| `type` | Shape | Produces | References |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| omitted | [`RegularColorDef`](#regularcolordef) | An opaque color, or a fixed `opacity` | `base` |
|
|
35
|
+
| `'shadow'` | [`ShadowColorDef`](#shadowcolordef--glowcolordef) | A translucent dark overlay | `bg`, `fg` |
|
|
36
|
+
| `'glow'` | [`GlowColorDef`](#shadowcolordef--glowcolordef) | A translucent light overlay | `bg`, `fg` |
|
|
37
|
+
| `'mix'` | [`MixColorDef`](#mixcolordef) | A blend of two colors, opaque or translucent | `base`, `target` |
|
|
38
|
+
|
|
39
|
+
Rules shared by all four:
|
|
40
|
+
|
|
41
|
+
- A reference names another color in the **same theme**. A missing name, or a cycle through `base` / `bg` / `fg` / `target`, throws when the theme first resolves.
|
|
42
|
+
- Shadows and glows are leaves: no color can reference one, and they can't reference each other.
|
|
43
|
+
- Colors resolve in dependency order, so you can define them in any order.
|
|
44
|
+
- Every shape accepts [`pastel`](#per-color-pastel) and [`inherit`](#inherit).
|
|
45
|
+
|
|
46
|
+
Every validation rule and its error is listed in [Validation](./configuration.md#validation), and every warning in [Warnings](./configuration.md#warnings).
|
|
47
|
+
|
|
48
|
+
## `RegularColorDef`
|
|
49
|
+
|
|
50
|
+
A color placed by tone, either on its own (a **root**) or relative to another color (a **dependent**).
|
|
51
|
+
|
|
52
|
+
| Field | Type | Default | Description |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| `from` | `GlazeColorValue` | — | Seed the color from a literal value. Supplies the hue, the tone, and an **absolute** saturation. See [`from`](#from). |
|
|
55
|
+
| `tone` | `HCPair<ToneValue>` | `from`'s tone, else the base's tone | Number: absolute tone 0–100. `'+N'` / `'-N'`: a tone delta from `base` (requires `base`). `'max'` / `'min'`: the scheme's tone extreme. See [Tone values](#tone-values). |
|
|
56
|
+
| `saturation` | `number` | `1` | Factor (0–1) of the theme's saturation seed. Clamped to 0–1. With `from`, it replaces the color's absolute saturation and is still a factor of the seed. |
|
|
57
|
+
| `hue` | `number \| RelativeValue` | The seed hue (or `from`'s hue) | Number: absolute hue 0–360. `'+N'` / `'-N'`: relative to the theme seed hue, never to a base. See [Hue and saturation](#hue-and-saturation). |
|
|
58
|
+
| `darkHue` | `number \| RelativeValue` | `hue` | Hue for the `dark` and `darkContrast` variants. A relative value is relative to the theme's dark seed hue. See [Dark seed](#dark-seed-darkhue--darksaturation). |
|
|
59
|
+
| `darkSaturation` | `number` | `saturation` | Factor (0–1) of the dark saturation seed for the dark variants. When set, `darkDesaturation` is not applied on top. |
|
|
60
|
+
| `base` | `string` | — | Another color in the same theme. Makes this a dependent color. Can't be a shadow or glow. |
|
|
61
|
+
| `contrast` | `HCPair<ContrastSpec>` | — | Contrast floor against `base`, WCAG or APCA. Requires `base`. See [`contrast`](#contrast). |
|
|
62
|
+
| `mode` | `AdaptationMode` | `'auto'` | How the color adapts between light and dark. See [`mode`](#mode). |
|
|
63
|
+
| `autoFlip` | `boolean` | The config's `autoFlip` (`true`) | Mirror out-of-range results to the other side of the base instead of clamping. See [`autoFlip`](#autoflip). |
|
|
64
|
+
| `opacity` | `number` | `1` | Fixed alpha, 0–1. See [`opacity`](#opacity). |
|
|
65
|
+
| `pastel` | `boolean` | The theme's `pastel` (`false`) | Use the hue-independent safe chroma limit for this color. See [Per-color `pastel`](#per-color-pastel). |
|
|
66
|
+
| `role` | `RoleInput` | Inferred | How the color is used against `base`; fixes APCA polarity. See [Roles](#roles). |
|
|
67
|
+
| `inherit` | `boolean` | `true` | Whether `extend()` copies the color into child themes. See [`inherit`](#inherit). |
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { glaze } from '@tenphi/glaze';
|
|
71
|
+
|
|
72
|
+
const theme = glaze(280, 80);
|
|
73
|
+
|
|
74
|
+
theme.colors({
|
|
75
|
+
// root
|
|
76
|
+
surface: { tone: 97, saturation: 0.1 },
|
|
77
|
+
// dependent
|
|
78
|
+
'surface-text': { base: 'surface', tone: '-52', contrast: 'AA' },
|
|
79
|
+
// HC pair
|
|
80
|
+
border: { base: 'surface', tone: ['-8', '-16'] },
|
|
81
|
+
// root that keeps its tone in dark
|
|
82
|
+
accent: { tone: 52, mode: 'fixed' },
|
|
83
|
+
// literal value
|
|
84
|
+
brand: { from: '#2f5bff', base: 'surface', contrast: 3 },
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Root or dependent.** A color with an absolute `tone` (a number, `'max'`, or `'min'`) or a `from` value and no `base` is a root. A color with a `base` is dependent. Every regular color needs one of the three, or validation throws `glaze: color "<name>" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent).`
|
|
89
|
+
|
|
90
|
+
### `from`
|
|
91
|
+
|
|
92
|
+
`from` places a color at a literal value instead of deriving it from the seed: for brand colors, white-label and multi-tenant themes, and imported design tokens, where the value is a contract rather than a starting point.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const theme = glaze(280, 80);
|
|
96
|
+
|
|
97
|
+
theme.colors({
|
|
98
|
+
surface: { tone: 100, saturation: 0.12 },
|
|
99
|
+
brand: { from: '#2f5bff', base: 'surface', contrast: 3 },
|
|
100
|
+
});
|
|
101
|
+
// brand, light: #2f5bff (it already clears 3:1 on white, so it is kept)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It accepts every [`GlazeColorValue`](./color-tokens.md#glazecolorvalue) form: hex, `rgb()`, `hsl()`, `okhsl()`, `okhst()`, `oklch()`, or a value object. It supplies three things: `hue`, `tone`, and, unlike any other theme color, an **absolute saturation**. Every other color's `saturation` is a factor of the seed, so the seed is a ceiling. A `from` color carries its own chroma, so it can be more saturated than its theme and doesn't move when the seed changes:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
for (const seed of [5, 50, 100]) {
|
|
108
|
+
const theme = glaze(280, seed);
|
|
109
|
+
theme.colors({ brand: { from: '#2f5bff' } });
|
|
110
|
+
// brand, light: #2f5bff at every seed
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Aspect | Behavior |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| Which variant is exact | The **light, normal-contrast** variant reproduces the value: the color resolves with a local `lightTone: false`, the default the value forms of `glaze.color()` use. Dark and high contrast adapt as usual: those are the variants a reader picks when the normal one doesn't work for them, so readability outranks fidelity. To pin the value in every scheme, add `mode: 'static'`. |
|
|
117
|
+
| `contrast` | Still a floor in every scheme. A value that clears it is emitted untouched; one that misses moves only as far as the floor. The floor is solved per scheme, so which schemes come out exact depends on the color. |
|
|
118
|
+
| `tone` | Overrides the tone the value carried. Without `tone`, the color sits at the value's tone, even when it has a `base`. |
|
|
119
|
+
| `hue` | An absolute `hue` replaces the value's hue and keeps its saturation and tone. A relative `hue` is relative to the theme seed hue, as on any color, not to the value's hue. |
|
|
120
|
+
| `saturation` | Reverts to its usual meaning: a factor of the seed, not of the value. |
|
|
121
|
+
| Dark saturation | Without `saturation` or `darkSaturation` on the def: starts from the value's own saturation, and `darkDesaturation` applies, even when the theme has a dark saturation seed. With a def-level `saturation`, it is a factor of the dark seed when one is set (no reduction), else of the seed with `darkDesaturation`. A def-level `darkSaturation` is a factor of the dark seed, taken literally. |
|
|
122
|
+
| Dark hue | The value's hue, unless `hue` or `darkHue` is set. |
|
|
123
|
+
| `base` | Not needed: a `from` color is a root on its own. Add `base` + `contrast` when it has to stay legible against something. |
|
|
124
|
+
|
|
125
|
+
An unparsable value throws `glaze: color "<name>" has an invalid "from" value.` followed by the parser's message.
|
|
126
|
+
|
|
127
|
+
The concept and a worked example are in [Contrast solving](../contrast.mdx#literal-colors-with-a-floor).
|
|
128
|
+
|
|
129
|
+
### Tone values
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
type RelativeValue = `+${number}` | `-${number}`;
|
|
133
|
+
type ExtremeValue = 'max' | 'min';
|
|
134
|
+
type ToneValue = number | RelativeValue | ExtremeValue;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`tone` is on the contrast-uniform OKHST axis, 0–100: equal tone differences give equal WCAG contrast for neutrals, and chromatic colors can drift slightly in measured luminance. The model is in [Tone and OKHST](../okhst.mdx#tone-values).
|
|
138
|
+
|
|
139
|
+
| Form | Example | Meaning |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| Number (absolute) | `tone: 45` | Absolute tone 0–100. |
|
|
142
|
+
| Delta | `tone: '-52'` | Signed difference from the base's resolved tone. Requires `base`. |
|
|
143
|
+
| Extreme | `tone: 'max'` / `tone: 'min'` | The scheme's highest (`'max'` = author tone 100) or lowest (`'min'` = 0) tone. No `base` needed. |
|
|
144
|
+
| HC pair | `tone: ['-7', '-20']` | `[normal, highContrast]`. A single value applies to both. |
|
|
145
|
+
|
|
146
|
+
**Absolute tone.** On a root, the tone is mapped into each scheme by its [`mode`](#mode). On a dependent, it positions the color independently of the base: each scheme maps it on its own, and a `contrast` floor acts as the safety net.
|
|
147
|
+
|
|
148
|
+
**Delta.** Adds a signed tone delta to the base's resolved tone. In `light` and `lightContrast` the step is exact, which gives an equal contrast step for neutrals and a stable visual progression for chromatic colors. Dark depends on the color's `mode`:
|
|
149
|
+
|
|
150
|
+
- `'auto'` (default): Glaze steps from the base's **anchor**, where the base sits on the authored scale, and inverts and remaps the result through the dark window, so dark text on a light surface becomes light text on a dark one. The anchor is the base's authored tone when the base is an `'auto'` color placed by a tone, and its base's anchor plus the delta when it is an `'auto'` delta. For any other base — `'fixed'` or `'static'`, or an `'auto'` base placed by a contrast floor, a mix, or a `'max'` / `'min'` on a base — it is the base's own dark tone read back through the dark window, so the dependent mirrors around wherever the base sits (`'+10'` on a fixed tone-30 base steps −8.7 in dark). Full table: [Adaptation modes](./configuration.md#adaptation-modes).
|
|
151
|
+
- `'fixed'` and `'static'`: the delta is applied to the base's tone in each scheme, sign kept.
|
|
152
|
+
|
|
153
|
+
At the reference dark `eps`, every `'auto'` dark step is the dark window's scale times the delta, about 0.87× at the defaults: on a tone-97 surface, `'-10'` becomes +8.7 in dark and `'-52'` becomes +45.1; on a tone-50 base, `'+4'` becomes −3.5 and `'-4'` becomes +3.5; a `'-4'` hover on an `'AAA'` button steps +3.5. In `darkContrast` the same step is exact (±N), under any light `eps`. A custom dark `eps` reshapes the dark curve in `dark` and `darkContrast` alike, so a dark delta lands where the same authored tone would instead of at scale × delta. Near the ends of the scale the clamps below apply. See [How relative tone adapts](../schemes.mdx#how-relative-tone-adapts).
|
|
154
|
+
|
|
155
|
+
If `base + delta` falls outside `[0, 100]`:
|
|
156
|
+
|
|
157
|
+
- with `autoFlip` (the default), the delta is mirrored to the other side of the base;
|
|
158
|
+
- if the mirrored target is also out of range, or `autoFlip` is off, the result is clamped on the authored side.
|
|
159
|
+
|
|
160
|
+
The flip is decided on the light tone, and dark mirrors whichever side light took. Two clamps apply in dark only:
|
|
161
|
+
|
|
162
|
+
- **Step off the scale.** Dark steps from the anchor, so a step that fits from the base's light tone can still run past 0 or 100 from the anchor; dark then stops at the dark window's edge. An `'auto'` base placed by a tone meets this at the bottom, because the light window lifts authored tone 0 to 4.95: `'-4'` on tone 2 steps about +1.7 in dark, on tone 0 not at all. A base anchored at its own dark tone can meet it at either end: in `glaze(280, 40)`, a `'-4'` hover on `contrast: 12` text over a tone-97 surface steps +2.3 in dark, not +3.5. In `darkContrast` this applies only to a base anchored at its own dark tone, or under a custom light `eps`.
|
|
163
|
+
- **Base past the dark window.** A contrast floor or an extreme can push a base past the dark window, and a `'fixed'` delta can ride its base past it. A step from it is limited by the window edge or the base, whichever is further out: a step back toward the window moves the full scaled delta, one pointing further out stops at the base. On a tone-97 surface, `ink: { base: 'surface', tone: 'min' }` sits at tone 100 in dark; `'+4'` on it steps −3.5 there and `'-4'` stays at 100.
|
|
164
|
+
|
|
165
|
+
Add a `contrast` floor when a dark step must survive either clamp.
|
|
166
|
+
|
|
167
|
+
A delta needs `base`. When either entry of `tone` is a delta and there is no `base`, validation throws `glaze: color "<name>" has relative "tone" without "base".` That includes a delta only in the high-contrast entry (`tone: [50, '+20']`). With a `base`, the entries can mix forms: `tone: [50, '-20']` places the color at absolute tone 50 in the normal schemes and exactly 20 tones from its base in high contrast: below it in `lightContrast`, and above it in `darkContrast` under `mode: 'auto'`.
|
|
168
|
+
|
|
169
|
+
**Extreme without a base.** Flows through the scheme mapping like an absolute tone. Under `mode: 'auto'` it inverts in dark: `'max'` is the lightest tone in light and the darkest in dark. Use `mode: 'static'` to keep the same extreme in every scheme, or `mode: 'fixed'` to keep the same end without inverting.
|
|
170
|
+
|
|
171
|
+
**Extreme with a base.** Keeps the pair's contrast across schemes instead of squeezing it through the dark window. In dark, Glaze replays the tone shift the light scheme applied between the base and the extreme against the base's dark tone: mirrored under `mode: 'auto'`, same sign under `'fixed'`. The result is clamped to `[0, 100]` only, so it can cross the `darkTone` window edge, and pins at 100 or 0 when the shift doesn't fit. `mode: 'static'` is unaffected. The rule and a worked example are in [Extremes in dark](../schemes.mdx#extremes-in-dark).
|
|
172
|
+
|
|
173
|
+
**No tone.** A dependent with `base` and no `tone` takes the base's tone (a delta of 0). A `from` color takes the value's tone.
|
|
174
|
+
|
|
175
|
+
### Hue and saturation
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const theme = glaze(280, 80);
|
|
179
|
+
|
|
180
|
+
theme.colors({
|
|
181
|
+
surface: { tone: 97 },
|
|
182
|
+
'gradient-end': { tone: 90, hue: '+20' }, // 280 + 20 = 300
|
|
183
|
+
warning: { tone: 60, hue: 40 }, // absolute
|
|
184
|
+
'muted-fill': { tone: 90, saturation: 0.3 }, // 30% of seed saturation
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- A relative `hue` is relative to the **theme seed hue**, never to a base color. Hues wrap to 0–360.
|
|
189
|
+
- `saturation` is a 0–1 factor of the theme's 0–100 saturation seed. Values outside 0–1 are clamped.
|
|
190
|
+
- A `from` color uses its own hue and absolute saturation unless these fields are set; see [`from`](#from).
|
|
191
|
+
|
|
192
|
+
### Dark seed (`darkHue` / `darkSaturation`)
|
|
193
|
+
|
|
194
|
+
Tone inverts between light and dark, but hue and saturation don't: by default every scheme uses the one seed, and dark gets the flat `darkDesaturation` reduction. When dark needs its own chroma, give the theme a [dark seed](./theme.md#glazethemeseed), override single colors, or both:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const theme = glaze({
|
|
198
|
+
hue: 280,
|
|
199
|
+
saturation: 80,
|
|
200
|
+
darkHue: 268, // dark variants seed from 268 instead of 280
|
|
201
|
+
darkSaturation: 65, // and from 65 instead of 80
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
theme.colors({
|
|
205
|
+
surface: { tone: 97 },
|
|
206
|
+
accent: { tone: 55, hue: '+20', darkHue: '+35' },
|
|
207
|
+
warning: { tone: 60, saturation: 0.9, darkSaturation: 0.6 },
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Both fields apply to the `dark` **and** `darkContrast` variants.
|
|
212
|
+
|
|
213
|
+
| Rule | Behavior |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| Units | The seed's `darkSaturation` is 0–100, like the theme `saturation`. A color's `darkSaturation` is a 0–1 factor, like a color's `saturation`. |
|
|
216
|
+
| Fallback | `darkHue` falls back to `hue` and `darkSaturation` to `saturation`; a color falls back to the seed. Omitting everything gives the single-seed behavior. |
|
|
217
|
+
| Relative hue | `darkHue: '+N'` is relative to the theme's **dark** seed hue. A plain `hue: '+N'` with no `darkHue` is also re-applied to the dark seed hue, so the whole palette rotates together. |
|
|
218
|
+
| Absolute hue | An absolute `hue` with no `darkHue` is used as is in dark: absolute means absolute in every scheme. |
|
|
219
|
+
| `darkDesaturation` | Skipped as soon as any explicit dark saturation is authored, on the seed or on the color. The value you write is the value you get. A [`from`](#from) color with no `saturation` or `darkSaturation` of its own is the exception: a seed-level `darkSaturation` doesn't stop its reduction. |
|
|
220
|
+
| `mode: 'static'` | Ignores both. A static color keeps one hue and saturation in every scheme. |
|
|
221
|
+
| Shadows, glows, and mixes | Have no channels of their own. They derive hue and saturation from `bg` / `fg` / `base` / `target`, so they follow dark overrides automatically. |
|
|
222
|
+
|
|
223
|
+
With [`splitHue`](./output.md#dark-hues), a dark seed makes the hue custom properties scheme-dependent, and Glaze re-declares them for dark automatically. The concept is in [Light, dark, and high contrast](../schemes.mdx#dark-seed).
|
|
224
|
+
|
|
225
|
+
### `contrast`
|
|
226
|
+
|
|
227
|
+
`contrast` sets a minimum contrast against `base`, solved separately in every scheme:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
theme.colors({
|
|
231
|
+
surface: { tone: 97 },
|
|
232
|
+
'surface-text': { base: 'surface', tone: '-52', contrast: 'AA' },
|
|
233
|
+
'muted-text': {
|
|
234
|
+
base: 'surface',
|
|
235
|
+
tone: '-40',
|
|
236
|
+
contrast: { apca: 'content' },
|
|
237
|
+
},
|
|
238
|
+
'accent-text': { base: 'surface', contrast: { wcag: [4.5, 7] } },
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The type is `HCPair<ContrastSpec>`: a WCAG ratio or preset, or an `{ apca }` Lc target, optionally as a `[normal, highContrast]` pair. Every form, preset, and the automatic high-contrast escalation are in [Contrast and high contrast](./contrast.md#contrastspec). How the solver searches tone is in [Contrast solving](../contrast.mdx#how-the-solver-finds-a-tone).
|
|
243
|
+
|
|
244
|
+
### `autoFlip`
|
|
245
|
+
|
|
246
|
+
`autoFlip` decides what happens when a result falls outside its valid range:
|
|
247
|
+
|
|
248
|
+
- **Delta overshoot.** When `base ± delta` leaves `[0, 100]`, the delta is mirrored to the other side of the base (`'+30'` becomes `'-30'`) instead of clamping to the boundary. If the mirrored target is also out of range, the original delta is kept and clamped on the authored side. Light decides the side and dark mirrors it, with the dark-only clamps under [Tone values](#tone-values).
|
|
249
|
+
- **`contrast` direction.** When the requested direction can't meet the floor, the solver tries the opposite side of the base and uses it if it passes.
|
|
250
|
+
|
|
251
|
+
It defaults to the theme's effective `autoFlip`, which is `true` unless changed with `glaze.configure()` or a [config override](./theme.md#per-theme-config-override). Set `autoFlip: false` on a color to keep a delta on the authored side, or to pin an unmet floor to that direction's extreme (with a warning) instead of crossing over. Mixes have no per-color `autoFlip`; their contrast solve follows the theme's. See [Contrast solving](../contrast.mdx#autoflip-crossing-to-the-other-side).
|
|
252
|
+
|
|
253
|
+
### `mode`
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
type AdaptationMode = 'auto' | 'fixed' | 'static';
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
| Mode | In dark |
|
|
260
|
+
| --- | --- |
|
|
261
|
+
| `'auto'` (default) | Tone inverts (`100 − t`), then maps into the dark tone window. |
|
|
262
|
+
| `'fixed'` | Tone maps into the dark window without inverting. For brand fills, CTAs, and status banners. |
|
|
263
|
+
| `'static'` | The same tone, hue, and saturation in every scheme, with no tone window. |
|
|
264
|
+
|
|
265
|
+
High-contrast variants use the full tone range in every mode, and `'auto'` still inverts in `darkContrast`. Each mode in every scheme, with visuals and when to use it, is in [Light, dark, and high contrast](../schemes.mdx#adaptation-modes); which mapping steps each mode applies is in [Adaptation modes](./configuration.md#adaptation-modes), and the formulas and per-variant tones in [Scheme mapping](./configuration.md#scheme-mapping). How each mode treats a tone delta is under [Tone values](#tone-values).
|
|
266
|
+
|
|
267
|
+
### `opacity`
|
|
268
|
+
|
|
269
|
+
A fixed alpha, 0–1. The output color includes it:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
theme.colors({
|
|
273
|
+
scrim: { tone: 0, saturation: 0, mode: 'static', opacity: 0.5 },
|
|
274
|
+
});
|
|
275
|
+
// scrim: oklch(0 0 0 / 0.5) in every scheme
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Opacity doesn't take part in contrast solving: a translucent color has no fixed perceived tone. Combining it with `contrast` emits a [contrast and opacity](./configuration.md#contrast-and-opacity) warning:
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
glaze: color "<name>" has both "contrast" and "opacity".
|
|
282
|
+
Opacity makes perceived tone unpredictable.
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
For an alpha that is solved against what it covers, use a [shadow or glow](#shadowcolordef--glowcolordef) or a [transparent mix](#mixcolordef).
|
|
286
|
+
|
|
287
|
+
### Per-color `pastel`
|
|
288
|
+
|
|
289
|
+
`pastel` toggles the hue-independent "safe" chroma limit for one color. It applies to every OKHSL ↔ sRGB conversion that touches the color: luminance during contrast solving, gamut clamping at sRGB blend and mix edges, and output formatting. The effective flag is carried on the resolved variant (`ResolvedColorVariant.pastel`), so formatting uses the same gamut mapping as resolution.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const theme = glaze(280, 80);
|
|
293
|
+
|
|
294
|
+
theme.colors({
|
|
295
|
+
plain: { tone: 50, saturation: 1 },
|
|
296
|
+
soft: { tone: 50, saturation: 1, pastel: true },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
theme.resolve().get('soft')!.light.pastel; // → true
|
|
300
|
+
theme.css({ format: 'rgb' }).light;
|
|
301
|
+
// → '--plain-color: rgb(105.64 102.82 227.87);\n' +
|
|
302
|
+
// '--soft-color: rgb(113.43 117.68 169.27);'
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Omit it to use the theme's `pastel` (from its [config override](./theme.md#per-theme-config-override), default `false`). `pastel` can't be set with `glaze.configure()`.
|
|
306
|
+
|
|
307
|
+
The flag is part of the definition, so `extend()` copies it into child themes. Override it on the child to switch one color back:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
const parent = glaze(280, 80);
|
|
311
|
+
parent.colors({ soft: { tone: 50, saturation: 1, pastel: true } });
|
|
312
|
+
|
|
313
|
+
const child = parent.extend({
|
|
314
|
+
colors: { soft: { tone: 50, saturation: 1, pastel: false } },
|
|
315
|
+
});
|
|
316
|
+
child.resolve().get('soft')!.light.pastel; // → false
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
- **Shadows and glows**: `pastel` sets the gamut used to convert the pigment for the opacity solve and for output. The surface (`bg`) keeps its own flag.
|
|
320
|
+
- **Mixes**: `pastel` sets the gamut of the mixed result. The base and target keep their own flags, and the mix reads each in its gamut through the color it paints: a standard mix paints a pastel base or target exactly, and a pastel mix caps a vivid one at the pastel limit, keeping its hue and lightness. A transparent mix's overlay is the target in that gamut. The contrast solver measures the color the mix paints. See [Pastel mixes](../mixes.mdx#pastel-mixes).
|
|
321
|
+
- **Color tokens**: the same field exists on both `glaze.color()` input forms and survives the `export()` / `colorFrom()` round-trip; see [Color tokens](./color-tokens.md).
|
|
322
|
+
- **`splitHue`** requires every emitted color to be pastel; see [Output formats](./output.md#hue-channel-splitting-splithue).
|
|
323
|
+
|
|
324
|
+
### Roles
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
type Role = 'text' | 'surface' | 'border';
|
|
328
|
+
type RoleInput =
|
|
329
|
+
| Role
|
|
330
|
+
| 'bg' | 'background' | 'fill' | 'canvas' | 'paper' | 'layer'
|
|
331
|
+
| 'fg' | 'foreground' | 'content' | 'ink' | 'label' | 'stroke'
|
|
332
|
+
| 'divider' | 'outline' | 'separator' | 'hairline' | 'rule';
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
A color's `role` says how it is used against its `base`, and fixes **APCA polarity**: which of the two is measured as text. APCA is asymmetric (the Lc of `a` on `b` differs from `b` on `a`), so the role picks the argument order. WCAG is symmetric and ignores roles.
|
|
336
|
+
|
|
337
|
+
| Role | Polarity | Use | Aliases and name keywords |
|
|
338
|
+
| --- | --- | --- | --- |
|
|
339
|
+
| `'text'` | Foreground | Text, icons, foreground content | `text`, `fg`, `foreground`, `content`, `ink`, `label`, `stroke` |
|
|
340
|
+
| `'border'` | Foreground | Non-text spot elements: borders, dividers, outlines | `border`, `divider`, `outline`, `separator`, `hairline`, `rule` |
|
|
341
|
+
| `'surface'` | Background | Backgrounds and fills | `surface`, `bg`, `background`, `fill`, `canvas`, `paper`, `layer` |
|
|
342
|
+
|
|
343
|
+
A foreground color is measured as text over its base. A surface color is measured as the background, with its base as the text on top.
|
|
344
|
+
|
|
345
|
+
Each color's role is the first of:
|
|
346
|
+
|
|
347
|
+
1. An explicit `role`, normalized from any alias above.
|
|
348
|
+
2. When `inferRole` is on (the default), a role inferred from the color's name. The name is split on non-alphanumeric characters and camelCase humps, and the **last** recognized keyword wins: `button-text` → `text`, `input-bg` → `surface`, `card-outline` → `border`, `inputBg` → `surface`.
|
|
349
|
+
3. The opposite of the base's role: a color on a `surface` is `text`; a color on `text` or a `border` is a `surface`.
|
|
350
|
+
4. `'text'`: the base is treated as the background.
|
|
351
|
+
|
|
352
|
+
Turn name inference off with `glaze.configure({ inferRole: false })` or a per-theme config override; steps 1, 3, and 4 still apply. Shadows and glows always resolve as `'surface'`. Mixes take the same `role` field and chain. Standalone `glaze.color()` tokens don't: their role is an explicit `role` or `'text'`, never inferred from `name` (see [Roles on tokens](./color-tokens.md#roles-on-tokens)). The concept, with a polarity demo and an example, is in [Contrast solving](../contrast.mdx#roles-and-apca-polarity). The role helpers (`normalizeRole`, `inferRoleFromName`, `roleToPolarity`, `oppositeRole`) are in [Role helpers](./utilities.md#role-helpers).
|
|
353
|
+
|
|
354
|
+
### `inherit`
|
|
355
|
+
|
|
356
|
+
`theme.extend()` copies every color into the child unless it has `inherit: false`:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
const primary = glaze(280, 80);
|
|
360
|
+
primary.colors({
|
|
361
|
+
surface: { tone: 97 },
|
|
362
|
+
// stays on primary only
|
|
363
|
+
'brand-mark': { from: '#2f5bff', inherit: false },
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const danger = primary.extend({ hue: 23 }); // has surface, not brand-mark
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
A child that inherits a color whose `base` was not inherited fails validation (`references non-existent base`). Redefine the base in the child's `colors`, or mark the dependent `inherit: false` too. `inherit` works the same on shadows, glows, and mixes. See [`theme.extend()`](./theme.md#themeextendoptions).
|
|
370
|
+
|
|
371
|
+
## `ShadowColorDef` / `GlowColorDef`
|
|
372
|
+
|
|
373
|
+
Translucent overlays. Each emits a pigment (black for a shadow, white for a glow) with an opacity solved so that, once the browser composites it over `bg`, the surface lands exactly `intensity` tones darker (`shadow`) or lighter (`glow`). The two types share one shape; `type` picks the direction, which is fixed in every scheme.
|
|
374
|
+
|
|
375
|
+
| Field | Type | Default | Description |
|
|
376
|
+
| --- | --- | --- | --- |
|
|
377
|
+
| `type` | `'shadow' \| 'glow'` | — | Discriminator. A `shadow` always darkens and a `glow` always lightens, in light and dark. |
|
|
378
|
+
| `bg` | `string` | — | The surface the overlay falls on. Required. Can't be a shadow or glow. |
|
|
379
|
+
| `fg` | `string` | — | The object casting the shadow or emitting the glow, such as the card. Scales the step: a darker object casts a deeper shadow, a lighter one a stronger glow. Omit for an exact shade or tint of `intensity` tones. Can't be a shadow or glow. |
|
|
380
|
+
| `intensity` | `HCPair<number>` | — | Tone step, 0–100: how many tones darker (shadow) or lighter (glow) than `bg` the composite lands. Clamped to 0–100. Required. |
|
|
381
|
+
| `tuning` | `ShadowTuning` / `GlowTuning` | — | Per-color [tuning](#lightingtuning), merged field by field over the global `shadowTuning` / `glowTuning`. A glow's can set [`headroom`](#lightingtuning); on a shadow's, `headroom` throws. |
|
|
382
|
+
| `pastel` | `boolean` | The theme's `pastel` | Gamut for the pigment. See [Per-color `pastel`](#per-color-pastel). |
|
|
383
|
+
| `inherit` | `boolean` | `true` | See [`inherit`](#inherit). |
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
const theme = glaze(280, 80);
|
|
387
|
+
|
|
388
|
+
theme.colors({
|
|
389
|
+
surface: { tone: 97 },
|
|
390
|
+
card: { tone: 100 },
|
|
391
|
+
accent: { tone: 45 },
|
|
392
|
+
|
|
393
|
+
'card-shadow': {
|
|
394
|
+
type: 'shadow',
|
|
395
|
+
bg: 'surface',
|
|
396
|
+
fg: 'card',
|
|
397
|
+
intensity: 8,
|
|
398
|
+
},
|
|
399
|
+
'surface-hover': { type: 'shadow', bg: 'surface', intensity: 5 },
|
|
400
|
+
'accent-pressed': { type: 'shadow', bg: 'accent', intensity: [8, 14] },
|
|
401
|
+
'accent-hover': { type: 'glow', bg: 'accent', intensity: 6 },
|
|
402
|
+
});
|
|
403
|
+
// 'card-shadow', light: oklch(0 0 0 / 0.1066)
|
|
404
|
+
// 'accent-hover', light: oklch(1 0 0 / 0.0948)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
- **Object factor.** With `fg`, the step is `intensity × k`, where `k = max(0, 1 + β · d · (t_bg − t_fg) / 100)`, `d = +1` for a shadow and `−1` for a glow, and `β` is `tuning.objectInfluence`. An object at the surface's own tone gives exactly `intensity`, as does omitting `fg`.
|
|
408
|
+
- **Per scheme.** `bg` and `fg` are read in each scheme, so the step and `k` adapt in dark and high contrast on their own.
|
|
409
|
+
- **HC pairs and the level.** `intensity` accepts `[normal, highContrast]` and is interpolated by a manual [`contrastLevel`](./contrast.md#manual-contrast-level).
|
|
410
|
+
- **Output.** Shadows and glows appear in every output method next to regular colors, with an alpha component. They have no `role` (always `'surface'`), no `mode`, and no channels of their own; `ResolvedColor.mode` is `undefined` for them.
|
|
411
|
+
- **Unreachable steps.** A glow on white or a deep shadow on near-black can't reach the step. The result pins to the pigment's strongest composite (full opacity, or transparent when the pigment can't move the surface at all) and emits a deduped `console.warn`. This includes the common case of a shadow on an `auto` light surface: a tone-97 surface sits at tone 3 in `darkContrast`, so `card-shadow` and `surface-hover` above pin to opaque black and warn there (`accent-pressed`, on a mid-tone accent, is unaffected). Use a glow for elevation in dark. A glow with a `headroom` can go past white instead; see [HDR values](#hdr-values-resolvedhdr).
|
|
412
|
+
- **Errors.** `glaze: shadow "<name>" is missing "bg".`, `glaze: shadow "<name>" references non-existent bg "<ref>".`, and `glaze: glow "<name>" fg "<ref>" references a shadow color.` (and the same for the other combinations).
|
|
413
|
+
|
|
414
|
+
How the opacity solve works, the [object-factor table](../shadows-and-glows.mdx#object-influence), state layers, and [dark-mode elevation](../shadows-and-glows.mdx#dark-mode-elevation) are in [Shadows and glows](../shadows-and-glows.mdx). The standalone functions are in [`glaze.shadow()` / `glaze.glow()`](./utilities.md#glazeshadowinput--glazeglowinput).
|
|
415
|
+
|
|
416
|
+
### `LightingTuning`
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
interface LightingTuning {
|
|
420
|
+
objectInfluence?: number;
|
|
421
|
+
tint?: number;
|
|
422
|
+
reach?: number;
|
|
423
|
+
hueBleed?: number;
|
|
424
|
+
}
|
|
425
|
+
type ShadowTuning = LightingTuning;
|
|
426
|
+
interface GlowTuning extends LightingTuning {
|
|
427
|
+
headroom?: number;
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
| Field | Default | Description |
|
|
432
|
+
| --- | --- | --- |
|
|
433
|
+
| `objectInfluence` | `0.5` | β in the object factor. `0` removes `fg`'s effect on the step (`k = 1`); `fg` still feeds `hueBleed`. `k` tops out at `1 + β`, for a black object on a white surface (shadow). |
|
|
434
|
+
| `tint` | `0` | Pigment saturation (0–1) in the surface's hue. `0` is neutral black or white. Shows only with `reach` below `1`, because pure black and white have no hue. On a neutral surface (saturation at most `1e-4`, which covers every gray given as a value) the tint takes the object's hue when `hueBleed` is above `0`, and the pigment stays neutral otherwise. |
|
|
435
|
+
| `reach` | `1` | Where the pigment sits: the fraction (0–1) of the way from the surface's tone to black (shadow) or white (glow). `1` is pure black or white, the most reach. Lower values let a `tint` show and cost reach. |
|
|
436
|
+
| `hueBleed` | `0` | Pull (0–1) of the pigment hue from the surface's hue toward the object's (`fg`) hue. Needs `fg`, a `tint`, and `reach` below `1` to show. |
|
|
437
|
+
| `headroom` | `0` | Glows only. Stops above SDR white a glow may use when its step crosses white: `1` allows up to 2× white in each linear channel, `2` up to 4×. A finite number ≥ 0; above 2 it clamps to 2 with a warning. On a shadow's tuning it throws. See [HDR values](#hdr-values-resolvedhdr) and [Glow headroom](./configuration.md#glow-headroom). |
|
|
438
|
+
|
|
439
|
+
`tint`, `reach`, and `hueBleed` are clamped to 0–1. A field that is set must be a finite number, or the tuning throws (`glaze: shadowTuning.reach must be a finite number (got NaN).`); a field set to `undefined` counts as unset. Each field resolves separately, later wins:
|
|
440
|
+
|
|
441
|
+
1. The defaults above.
|
|
442
|
+
2. The global `shadowTuning` (for shadows) or `glowTuning` (for glows), from `glaze.configure()`. A theme or token [config override](./configuration.md#glazeconfigoverride) that sets `shadowTuning` / `glowTuning` replaces the global object as a whole.
|
|
443
|
+
3. The color's `tuning`.
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
theme.colors({
|
|
447
|
+
'shadow-tinted': {
|
|
448
|
+
type: 'shadow',
|
|
449
|
+
bg: 'surface',
|
|
450
|
+
intensity: 8,
|
|
451
|
+
tuning: { tint: 0.3, reach: 0.85 },
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
glaze.configure({
|
|
456
|
+
shadowTuning: { objectInfluence: 0.4 },
|
|
457
|
+
glowTuning: { objectInfluence: 0.6 },
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
There is no opacity cap: when the step is reachable, it is exact. For a lighter overlay, lower `intensity`. What a tint costs on dark surfaces is shown in [Tinted pigment](../shadows-and-glows.mdx#tinted-pigment).
|
|
462
|
+
|
|
463
|
+
### HDR values (`ResolvedHdr`)
|
|
464
|
+
|
|
465
|
+
With a `headroom` above 0, a glow whose target tone (the surface's luminance tone + `intensity × k`) passes 100 while its SDR value is pinned at its brightest gets an HDR value on its resolved variant, next to the unchanged SDR one. What it is for and when it appears: [HDR glows](../hdr.mdx).
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
interface ResolvedHdr {
|
|
469
|
+
h: number; // base OKHSL hue, 0–360
|
|
470
|
+
s: number; // base OKHSL saturation, 0–1, never pastel
|
|
471
|
+
t: number; // base tone, 0–1
|
|
472
|
+
exposure: number; // gain over the base, in stops (> 0)
|
|
473
|
+
}
|
|
474
|
+
// ResolvedColorVariant.hdr?: ResolvedHdr
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
- **Base.** What SDR paints at its brightest, taken opaque: the pigment at full opacity, or a white surface (tone 100) that no pigment can lift. `s` is standard OKHSL saturation, even on a `pastel` variant. `variant.t` stays at most 1.
|
|
478
|
+
- **Exposure.** The HDR color is the base's linear sRGB times `2^exposure`, with `exposure = log2(min(Y(T*) / Y(base), 2^headroom / max channel))` and `T*` the target tone: the exact step when the headroom allows it, otherwise capped so no linear channel passes `2^headroom`, with a [warning](./configuration.md#shadow-or-glow-out-of-reach). A step the HDR value covers doesn't warn.
|
|
479
|
+
- **Absent elsewhere.** The `hdr` key is missing (not `undefined`) on regular colors, shadows, mixes, glows that stay below white, glows whose pigment can't lift a non-white surface at all, and every glow at `headroom: 0`. The edge cases: [When a glow gets an HDR value](../hdr.mdx#when-a-glow-gets-an-hdr-value) and [Crossing white](../hdr.mdx#crossing-white).
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
const theme = glaze(280, 80, { glowTuning: { headroom: 1 } });
|
|
483
|
+
theme.colors({
|
|
484
|
+
surface: { tone: 100 },
|
|
485
|
+
'surface-glow': { type: 'glow', bg: 'surface', intensity: 12 },
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const glow = theme.resolve().get('surface-glow')!;
|
|
489
|
+
glaze.format(glow.light); // 'oklch(1 0 0 / 0)' — SDR can't lift white
|
|
490
|
+
glow.light.hdr?.exposure; // 0.5489…
|
|
491
|
+
glaze.formatHdr(glow.light);
|
|
492
|
+
// → 'color(srgb 1.1813 1.1813 1.1813)', tone 112
|
|
493
|
+
glow.dark.hdr; // undefined — the dark surface leaves room below white
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
[`glaze.formatHdr()`](./utilities.md#glazeformathdrvariant-hdrformat) writes it as extended `color(srgb R G B)`, and [`hdrToSrgb()` / `hdrToLinearSrgb()`](./utilities.md#hdrtosrgbhdr--hdrtolinearsrgbhdr) return its channels. Exporters emit it only with `modes.hdr`; see [HDR output](./output.md#hdr-output).
|
|
497
|
+
|
|
498
|
+
## `MixColorDef`
|
|
499
|
+
|
|
500
|
+
A blend of two colors: an opaque interpolation, or the target laid over the base with an alpha.
|
|
501
|
+
|
|
502
|
+
| Field | Type | Default | Description |
|
|
503
|
+
| --- | --- | --- | --- |
|
|
504
|
+
| `type` | `'mix'` | — | Discriminator. |
|
|
505
|
+
| `base` | `string` | — | The "from" color. Required. Can't be a shadow or glow. |
|
|
506
|
+
| `target` | `string` | — | The "to" color. Required. Can't be a shadow or glow. |
|
|
507
|
+
| `value` | `HCPair<number>` | — | Mix ratio 0–100: `0` is pure base, `100` pure target. With `blend: 'transparent'`, the target's opacity (`value / 100`). Clamped to 0–100. Required. |
|
|
508
|
+
| `blend` | `'opaque' \| 'transparent'` | `'opaque'` | `'opaque'` produces a solid color. `'transparent'` produces the target color with an alpha, for the browser to composite over the base. |
|
|
509
|
+
| `space` | `'okhsl' \| 'srgb'` | `'okhsl'` | Interpolation space for `'opaque'`. `'okhsl'` interpolates hue (shortest arc), saturation, and lightness in OKHSL. `'srgb'` interpolates linear-light sRGB channels, like CSS `color-mix(in srgb-linear, …)`. Ignored for `'transparent'`. |
|
|
510
|
+
| `contrast` | `HCPair<ContrastSpec>` | — | Floor against `base`. The solver adjusts the ratio (opaque) or the opacity (transparent); an unreachable floor pins and [warns](./configuration.md#mix-contrast-unmet). See [Contrast and high contrast](./contrast.md#contrastspec). |
|
|
511
|
+
| `pastel` | `boolean` | The theme's `pastel` | Gamut of the mixed result. See [Per-color `pastel`](#per-color-pastel). |
|
|
512
|
+
| `role` | `RoleInput` | Inferred | Role of the result against `base`, for APCA polarity. Same chain as on a [regular color](#roles). |
|
|
513
|
+
| `inherit` | `boolean` | `true` | See [`inherit`](#inherit). |
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
theme.colors({
|
|
517
|
+
surface: { tone: 95 },
|
|
518
|
+
accent: { tone: 30 },
|
|
519
|
+
// black in every scheme
|
|
520
|
+
black: { tone: 0, saturation: 0, mode: 'static' },
|
|
521
|
+
|
|
522
|
+
'accent-tint': {
|
|
523
|
+
type: 'mix',
|
|
524
|
+
base: 'surface',
|
|
525
|
+
target: 'accent',
|
|
526
|
+
value: 30,
|
|
527
|
+
},
|
|
528
|
+
'surface-hover': {
|
|
529
|
+
type: 'mix',
|
|
530
|
+
base: 'surface',
|
|
531
|
+
target: 'black',
|
|
532
|
+
value: 8,
|
|
533
|
+
blend: 'transparent',
|
|
534
|
+
}, // the `black` color at alpha 0.08
|
|
535
|
+
'accent-wash': {
|
|
536
|
+
type: 'mix',
|
|
537
|
+
base: 'surface',
|
|
538
|
+
target: 'accent',
|
|
539
|
+
value: [10, 20],
|
|
540
|
+
contrast: 'AA',
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
- **Transparent output.** The color is the target's hue, saturation, and tone (in the mix's gamut when their [`pastel` flags](#per-color-pastel) differ) with `alpha = value / 100`. The browser composites it with `source-over` on gamma-encoded sRGB, and the contrast solve models exactly that, so a solved opacity renders at the contrast it was solved for.
|
|
546
|
+
- **Achromatic colors.** In `'okhsl'`, a side with a saturation of zero or of float noise up to 1e-6 (a color defined with `saturation: 0`, or a gray given as a value such as `'#f5f5f5'`, which parses with zero saturation) has no hue, so the mix takes the other side's hue, matching CSS `color-mix()` "missing component" behavior. A white or black that keeps real saturation still carries its hue. A mix of two zero-saturation colors stays gray in either space. See [Achromatic colors](../mixes.mdx#achromatic-colors).
|
|
547
|
+
- **Chaining.** A mix can use another mix as its `base` or `target`. A transparent mix or an `opacity` color is accepted as a mix's `base` / `target` or a shadow's or glow's `bg` / `fg`, but its alpha is ignored: its opaque color is used.
|
|
548
|
+
- **`autoFlip`.** Mixes have no per-color `autoFlip`; the contrast solve uses the theme's.
|
|
549
|
+
- **Adaptation.** A mix reads its base and target in each scheme, so it adapts to dark and high contrast with them. `ResolvedColor.mode` is `undefined` for mixes.
|
|
550
|
+
- **Errors.** `glaze: mix "<name>" references non-existent base "<ref>".` (or `target`), and `glaze: mix "<name>" base "<ref>" references a shadow color.` (or `glow`, or `target`).
|
|
551
|
+
|
|
552
|
+
Ramps of [opaque](../mixes.mdx#opaque-mix) and [transparent](../mixes.mdx#transparent-mix) mixes, [contrast on mixes](../mixes.mdx#contrast-on-mixes), and [when to use a mix](../mixes.mdx#mix-shadow-glow-or-opacity) instead of a shadow, glow, or `opacity` are in [Mixes and transparency](../mixes.mdx).
|