@tenphi/glaze 0.0.0-snapshot.07e5d83 → 0.0.0-snapshot.0fcd6cf
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 +169 -0
- package/README.md +108 -1015
- package/dist/index.cjs +5212 -1017
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1924 -220
- package/dist/index.d.mts +1924 -220
- package/dist/index.mjs +5178 -1017
- package/dist/index.mjs.map +1 -1
- package/docs/contrast.mdx +751 -0
- package/docs/exporting.mdx +851 -0
- package/docs/getting-started.mdx +488 -0
- package/docs/methodology.mdx +977 -0
- package/docs/migration.md +505 -0
- package/docs/mixes.mdx +305 -0
- package/docs/okhst.mdx +680 -0
- package/docs/reference/color-definitions.md +554 -0
- package/docs/reference/color-tokens.md +417 -0
- package/docs/reference/configuration.md +510 -0
- package/docs/reference/contrast.md +277 -0
- package/docs/reference/index.md +223 -0
- package/docs/reference/output.md +834 -0
- package/docs/reference/palette.md +435 -0
- package/docs/reference/theme.md +332 -0
- package/docs/reference/utilities.md +629 -0
- package/docs/schemes.mdx +637 -0
- package/docs/shadows-and-glows.mdx +737 -0
- package/package.json +20 -10
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shadows and glows
|
|
3
|
+
description: Translucent overlays that land an exact tone step on any surface with room for it, in every scheme.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import Example from '../docs-site/components/Example.astro';
|
|
7
|
+
import StepCompare from '../docs-site/components/shadows-and-glows/StepCompare.tsx';
|
|
8
|
+
import ObjectInfluence from '../docs-site/components/shadows-and-glows/ObjectInfluence.tsx';
|
|
9
|
+
import StateLayers from '../docs-site/components/shadows-and-glows/StateLayers.tsx';
|
|
10
|
+
import CardShadows from '../docs-site/components/shadows-and-glows/CardShadows.tsx';
|
|
11
|
+
import ElevationLadder from '../docs-site/components/shadows-and-glows/ElevationLadder.tsx';
|
|
12
|
+
import TintedPigment from '../docs-site/components/shadows-and-glows/TintedPigment.tsx';
|
|
13
|
+
import Unreachable from '../docs-site/components/shadows-and-glows/Unreachable.tsx';
|
|
14
|
+
import HdrGlows from '../docs-site/components/shadows-and-glows/HdrGlows.tsx';
|
|
15
|
+
|
|
16
|
+
export const stateTheme = {
|
|
17
|
+
hue: 280,
|
|
18
|
+
saturation: 80,
|
|
19
|
+
colors: {
|
|
20
|
+
surface: { tone: 100, saturation: 0.1 },
|
|
21
|
+
'surface-hover': { type: 'shadow', bg: 'surface', intensity: 4 },
|
|
22
|
+
'surface-pressed': { type: 'shadow', bg: 'surface', intensity: [8, 12] },
|
|
23
|
+
'accent-surface': { tone: 52, mode: 'fixed' },
|
|
24
|
+
'accent-surface-hover': { type: 'shadow', bg: 'accent-surface', intensity: 4 },
|
|
25
|
+
'accent-surface-pressed': { type: 'shadow', bg: 'accent-surface', intensity: [8, 12] },
|
|
26
|
+
'accent-surface-highlight': { type: 'glow', bg: 'accent-surface', intensity: 5 },
|
|
27
|
+
'inverse-surface': { tone: 18, saturation: 0.2, mode: 'fixed' },
|
|
28
|
+
'inverse-surface-hover': { type: 'glow', bg: 'inverse-surface', intensity: 4 },
|
|
29
|
+
'inverse-surface-pressed': { type: 'glow', bg: 'inverse-surface', intensity: [8, 12] },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const cardTheme = {
|
|
34
|
+
hue: 280,
|
|
35
|
+
saturation: 80,
|
|
36
|
+
colors: {
|
|
37
|
+
surface: { tone: 100, saturation: 0.1 },
|
|
38
|
+
card: { base: 'surface', tone: '-3', saturation: 0.15 },
|
|
39
|
+
'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'card', intensity: 3 },
|
|
40
|
+
'shadow-md': { type: 'shadow', bg: 'surface', fg: 'card', intensity: [6, 9] },
|
|
41
|
+
'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'card', intensity: [9, 14] },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const elevationTheme = {
|
|
46
|
+
hue: 280,
|
|
47
|
+
saturation: 80,
|
|
48
|
+
colors: {
|
|
49
|
+
surface: { tone: 92, saturation: 0.1 },
|
|
50
|
+
'elevation-1': { type: 'glow', bg: 'surface', intensity: 3 },
|
|
51
|
+
'elevation-2': { type: 'glow', bg: 'surface', intensity: 5 },
|
|
52
|
+
'elevation-3': { type: 'glow', bg: 'surface', intensity: 7 },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const hdrTheme = {
|
|
57
|
+
hue: 280,
|
|
58
|
+
saturation: 80,
|
|
59
|
+
config: { glowTuning: { headroom: 1 } },
|
|
60
|
+
colors: {
|
|
61
|
+
surface: { tone: 100 },
|
|
62
|
+
'surface-glow': { type: 'glow', bg: 'surface', intensity: 12 },
|
|
63
|
+
near: { tone: 95, saturation: 0, mode: 'static' },
|
|
64
|
+
'near-glow': { type: 'glow', bg: 'near', intensity: 12 },
|
|
65
|
+
inverse: { tone: 12, saturation: 0, mode: 'static' },
|
|
66
|
+
'inverse-glow': { type: 'glow', bg: 'inverse', intensity: 12 },
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
`shadow` and `glow` are translucent overlays. Each emits a pigment (black for a
|
|
71
|
+
shadow, white for a glow) with an opacity Glaze solves so that, once the
|
|
72
|
+
browser composites it over `bg`, the surface lands **exactly `intensity`
|
|
73
|
+
tones** darker (`shadow`) or lighter (`glow`), scaled by the object when you
|
|
74
|
+
name one. The step is the same on every surface and in every scheme where the
|
|
75
|
+
surface has room for it: light, mid, dark, and chromatic.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { glaze } from '@tenphi/glaze';
|
|
79
|
+
|
|
80
|
+
const theme = glaze(280, 80);
|
|
81
|
+
|
|
82
|
+
theme.colors({
|
|
83
|
+
surface: { tone: 97 },
|
|
84
|
+
card: { tone: 100 },
|
|
85
|
+
accent: { tone: 45 },
|
|
86
|
+
|
|
87
|
+
// Object-aware drop shadow: `fg` is the element casting it.
|
|
88
|
+
'card-shadow': {
|
|
89
|
+
type: 'shadow',
|
|
90
|
+
bg: 'surface',
|
|
91
|
+
fg: 'card',
|
|
92
|
+
intensity: 8,
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// No `fg`: an exact shade, for state layers and scrims.
|
|
96
|
+
'surface-hover': { type: 'shadow', bg: 'surface', intensity: 5 },
|
|
97
|
+
'accent-pressed': { type: 'shadow', bg: 'accent', intensity: [8, 14] },
|
|
98
|
+
|
|
99
|
+
// The mirror: dark-mode elevation, edge highlights, lightening states.
|
|
100
|
+
'accent-highlight': { type: 'glow', bg: 'accent', intensity: 4 },
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Resolving this sample logs two warnings. In dark high contrast the `auto`
|
|
105
|
+
surface sits at tone 3, so `card-shadow` and `surface-hover` can't darken it by
|
|
106
|
+
their full step: they pin to black and warn, even when you don't emit
|
|
107
|
+
high-contrast tokens. See [Unreachable steps](#unreachable-steps).
|
|
108
|
+
|
|
109
|
+
Every field is listed in
|
|
110
|
+
[Color definitions](./reference/color-definitions.md#shadowcolordef--glowcolordef);
|
|
111
|
+
this page explains how the overlays behave.
|
|
112
|
+
|
|
113
|
+
## The same step on every surface
|
|
114
|
+
|
|
115
|
+
`intensity` is a step on tone, the WCAG-shaped axis Glaze uses everywhere
|
|
116
|
+
(see [Tone and OKHST](./okhst.mdx)). For neutrals a tone step is a fixed WCAG
|
|
117
|
+
ratio, so 6 tones is about 1.20:1 wherever it lands, and the same number reads
|
|
118
|
+
the same on any surface. APCA cannot express steps this small, so overlays
|
|
119
|
+
are always measured in tone.
|
|
120
|
+
|
|
121
|
+
A fixed opacity cannot do this. Below, each surface carries a `shadow` with
|
|
122
|
+
`intensity: 6` (top patch) and a black layer at a fixed 8% (bottom patch); the
|
|
123
|
+
big number is the tone step each one actually paints.
|
|
124
|
+
|
|
125
|
+
<StepCompare
|
|
126
|
+
kind="shadow"
|
|
127
|
+
tones={[100, 70, 50, 25, 12]}
|
|
128
|
+
accentTone={50}
|
|
129
|
+
intensity={6}
|
|
130
|
+
fixedAlpha={0.08}
|
|
131
|
+
caption="Neutral surfaces at the listed tones, plus a hue-280 accent at tone 50. Top: shadow, intensity 6. Bottom: black at a fixed 8%. Big numbers are the measured tone step of the composite against its surface."
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
The solved shadow lands 6.0 tones everywhere, at an opacity that climbs as the
|
|
135
|
+
surface darkens. The fixed layer darkens white by almost 6 tones and a tone-12
|
|
136
|
+
surface by little more than one. Glows mirror this:
|
|
137
|
+
|
|
138
|
+
<StepCompare
|
|
139
|
+
kind="glow"
|
|
140
|
+
tones={[70, 50, 25, 12, 0]}
|
|
141
|
+
accentTone={50}
|
|
142
|
+
intensity={6}
|
|
143
|
+
fixedAlpha={0.08}
|
|
144
|
+
caption="Top: glow, intensity 6. Bottom: white at a fixed 8%. The fixed layer lifts every surface by a different amount; the solved glow lifts each one 6 tones."
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
## How the opacity is solved
|
|
148
|
+
|
|
149
|
+
Browsers composite `source-over` on gamma-encoded sRGB: 50% black over white
|
|
150
|
+
paints `#808080`, not the `#bcbcbc` that mixing linear light would give. Glaze
|
|
151
|
+
models exactly that. The composite's luminance changes monotonically with
|
|
152
|
+
opacity, so a bisection finds the opacity that lands the step, on chromatic
|
|
153
|
+
surfaces too.
|
|
154
|
+
|
|
155
|
+
The required opacity depends heavily on the surface. A 6-tone shadow needs
|
|
156
|
+
α 0.081 on white, 0.107 on a tone-50 surface, and 0.477 on a tone-10 surface.
|
|
157
|
+
|
|
158
|
+
There is no opacity cap: when the step is reachable, it is exact. For a
|
|
159
|
+
lighter overlay, lower `intensity`.
|
|
160
|
+
|
|
161
|
+
The output is the pigment with its solved alpha. Shadows and glows appear in
|
|
162
|
+
every output method (`tokens()`, `tasty()`, `css()`, `json()`, and the rest)
|
|
163
|
+
alongside regular colors. The default pigment is neutral, so the values stay
|
|
164
|
+
clean:
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
rgb(0 0 0 / 0.0814) shadow, 6 tones on white
|
|
168
|
+
oklch(1 0 0 / 0.0953) glow, 6 tones on a tone-50 surface
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The alpha is only meaningful over the surface it was solved for. Paint the
|
|
172
|
+
token on top of `bg` (as a `box-shadow` color, a state-layer background, or a
|
|
173
|
+
pseudo-element over the surface).
|
|
174
|
+
|
|
175
|
+
## Direction is fixed per type
|
|
176
|
+
|
|
177
|
+
A `shadow` darkens in light **and** dark schemes; a `glow` lightens in both.
|
|
178
|
+
Neither flips when the scheme does. Pick the type for the effect you want in
|
|
179
|
+
each scheme: for example, `shadow` for light-mode cards and `glow` for
|
|
180
|
+
dark-mode elevation. When one component needs a darkening layer in light mode
|
|
181
|
+
and a lightening one in dark mode, emit both tokens and wire each in the
|
|
182
|
+
scheme it belongs to. Both still resolve in all four schemes, so each one warns
|
|
183
|
+
wherever its surface has no room, including schemes where you don't use it
|
|
184
|
+
([Unreachable steps](#unreachable-steps)).
|
|
185
|
+
|
|
186
|
+
`bg` and `fg` are read per scheme, so the solved opacity adapts on its own as
|
|
187
|
+
the surface moves in dark and high contrast. See
|
|
188
|
+
[Light, dark, and high contrast](./schemes.mdx) for how surfaces move between
|
|
189
|
+
schemes.
|
|
190
|
+
|
|
191
|
+
## Object influence
|
|
192
|
+
|
|
193
|
+
`fg` is the object casting the shadow or emitting the glow, such as the card.
|
|
194
|
+
It scales the step the way bounce light does. With `fg`, the step is
|
|
195
|
+
`intensity × k`:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
k = max(0, 1 + β · d · (t_bg − t_fg) / 100)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`t_bg` and `t_fg` are the tones of the surface and the object, `d` is `+1` for
|
|
202
|
+
a `shadow` and `−1` for a `glow`, and `β` is `tuning.objectInfluence` (`0.5`
|
|
203
|
+
by default).
|
|
204
|
+
|
|
205
|
+
- An object at the surface's own tone gives exactly `intensity`, and so does
|
|
206
|
+
omitting `fg`.
|
|
207
|
+
- A darker object deepens its shadow; a lighter one softens it.
|
|
208
|
+
- For a glow it is the other way round: a lighter object glows stronger.
|
|
209
|
+
- `k` never drops below 0 and tops out at `1 + β` (a black object casting a
|
|
210
|
+
shadow on a white surface). `objectInfluence: 0` removes `fg`'s effect on
|
|
211
|
+
the step (`k = 1`); `fg` still feeds [`hueBleed`](#tinted-pigment).
|
|
212
|
+
|
|
213
|
+
Each card below casts a hard-edged `shadow` (offset, no blur) or emits a solid
|
|
214
|
+
`glow` ring, both with `intensity: 8` and `fg` pointed at the card:
|
|
215
|
+
|
|
216
|
+
<ObjectInfluence
|
|
217
|
+
intensity={8}
|
|
218
|
+
scenes={[
|
|
219
|
+
{
|
|
220
|
+
tone: 97,
|
|
221
|
+
label: 'Light surface · tone 97',
|
|
222
|
+
kinds: ['shadow'],
|
|
223
|
+
objects: [
|
|
224
|
+
{ label: 'white card', tone: 100 },
|
|
225
|
+
{ label: 'accent', tone: 45, chromatic: true },
|
|
226
|
+
{ label: 'near-black', tone: 8 },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
tone: 12,
|
|
231
|
+
label: 'Dark surface · tone 12',
|
|
232
|
+
kinds: ['shadow', 'glow'],
|
|
233
|
+
objects: [
|
|
234
|
+
{ label: 'raised card', tone: 20 },
|
|
235
|
+
{ label: 'accent', tone: 55, chromatic: true },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
]}
|
|
239
|
+
caption="Each label gives the object's tone, its factor k, and the step the composite lands (intensity 8, default β)."
|
|
240
|
+
/>
|
|
241
|
+
|
|
242
|
+
The same numbers as a table:
|
|
243
|
+
|
|
244
|
+
| Surface | Object | `k` (`shadow`) | `shadow` step | `glow` step |
|
|
245
|
+
| --- | --- | --- | --- | --- |
|
|
246
|
+
| light 97 | white card 100 | 0.985 | 7.9 | — |
|
|
247
|
+
| light 97 | accent 45 | 1.26 | 10.1 | — |
|
|
248
|
+
| light 97 | near-black 8 | 1.445 | 11.6 | — |
|
|
249
|
+
| dark 12 | raised card 20 | 0.96 | 7.7 | 8.3 |
|
|
250
|
+
| dark 12 | accent 55 | 0.785 | 6.3 | 9.7 |
|
|
251
|
+
|
|
252
|
+
Point `fg` at the element that casts the shadow, never at text: `fg` is the
|
|
253
|
+
object, not a contrast reference. Like `bg`, it must not be another shadow or
|
|
254
|
+
glow.
|
|
255
|
+
|
|
256
|
+
## State layers
|
|
257
|
+
|
|
258
|
+
Without `fg`, a shadow or glow is an exact shade or tint of its surface: the
|
|
259
|
+
right tool for hover and pressed states, which need the same visible change on
|
|
260
|
+
every fill.
|
|
261
|
+
|
|
262
|
+
<Example {...stateTheme} visual="none" />
|
|
263
|
+
|
|
264
|
+
<StateLayers
|
|
265
|
+
{...stateTheme}
|
|
266
|
+
rows={[
|
|
267
|
+
{
|
|
268
|
+
fill: 'surface',
|
|
269
|
+
label: 'Neutral fill',
|
|
270
|
+
layers: [
|
|
271
|
+
{ name: 'surface-hover', label: 'Hover' },
|
|
272
|
+
{ name: 'surface-pressed', label: 'Pressed' },
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
fill: 'accent-surface',
|
|
277
|
+
label: 'Accent fill',
|
|
278
|
+
layers: [
|
|
279
|
+
{ name: 'accent-surface-hover', label: 'Hover' },
|
|
280
|
+
{ name: 'accent-surface-pressed', label: 'Pressed' },
|
|
281
|
+
{ name: 'accent-surface-highlight', label: 'Highlight' },
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
fill: 'inverse-surface',
|
|
286
|
+
label: 'Dark fill',
|
|
287
|
+
layers: [
|
|
288
|
+
{ name: 'inverse-surface-hover', label: 'Hover' },
|
|
289
|
+
{ name: 'inverse-surface-pressed', label: 'Pressed' },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
]}
|
|
293
|
+
caption="Rest, hover, and pressed on three fills in your current scheme. Each number is the measured step of the layer over its fill."
|
|
294
|
+
/>
|
|
295
|
+
|
|
296
|
+
The accent fill uses `mode: 'fixed'`, so it stays a mid tone in every scheme
|
|
297
|
+
and its layers land the same steps everywhere. The dark fill uses glows, so
|
|
298
|
+
its states lighten it. The neutral `surface` follows the scheme: in dark high
|
|
299
|
+
contrast it becomes black, and a shadow has nothing left to darken, so
|
|
300
|
+
`surface-hover` and `surface-pressed` stay fully transparent and warn (the
|
|
301
|
+
figure shows what they reached; see [Unreachable steps](#unreachable-steps)).
|
|
302
|
+
A glow on this surface fails the other way: it warns in the light schemes,
|
|
303
|
+
where the surface is white. No overlay fits a surface that spans the whole
|
|
304
|
+
tone range. For a state that darkens in light schemes and lightens in dark
|
|
305
|
+
ones, use a regular color relative to the surface, such as
|
|
306
|
+
`{ base: 'surface', tone: '-4' }`, which flips with the scheme.
|
|
307
|
+
|
|
308
|
+
## Card drop shadows
|
|
309
|
+
|
|
310
|
+
For elevation in light schemes, define one shadow ladder with `bg` on the page
|
|
311
|
+
and `fg` on the card. Equal intensity steps read as evenly spaced on every
|
|
312
|
+
surface. Use [high-contrast pairs](#high-contrast) for shadows that should
|
|
313
|
+
deepen in high contrast.
|
|
314
|
+
|
|
315
|
+
<Example {...cardTheme} visual="none" />
|
|
316
|
+
|
|
317
|
+
<CardShadows
|
|
318
|
+
{...cardTheme}
|
|
319
|
+
page="surface"
|
|
320
|
+
card="card"
|
|
321
|
+
cards={[
|
|
322
|
+
{ shadow: 'shadow-sm', geometry: '0 0.0625rem 0.25rem 0' },
|
|
323
|
+
{ shadow: 'shadow-md', geometry: '0 0.375rem 1rem -0.125rem' },
|
|
324
|
+
{ shadow: 'shadow-lg', geometry: '0 1rem 2.5rem -0.5rem' },
|
|
325
|
+
]}
|
|
326
|
+
caption="Each card's box-shadow is colored by its token in your current scheme. The step is what the shadow lands where it is fully opaque."
|
|
327
|
+
/>
|
|
328
|
+
|
|
329
|
+
A blurred `box-shadow` fades out from the token's color, so the step is the
|
|
330
|
+
darkest the shadow gets, right at the card's edge. In dark schemes the page
|
|
331
|
+
sits near black and a dark card shadow has little room: the solved opacity
|
|
332
|
+
climbs, and in dark high contrast the page is black, so all three shadows
|
|
333
|
+
stay fully transparent and warn. For elevation in dark schemes, use glows on a
|
|
334
|
+
surface with room in every scheme, as in the next section.
|
|
335
|
+
|
|
336
|
+
## Dark-mode elevation
|
|
337
|
+
|
|
338
|
+
Glows are the mirror for dark-mode elevation and edge highlights: each level
|
|
339
|
+
lifts the surface a fixed number of tones. A glow resolves in all four
|
|
340
|
+
schemes, so it needs room above its surface in each of them. This ladder
|
|
341
|
+
sits on a light-gray surface, so the same tokens also lift cards toward white
|
|
342
|
+
in the light scheme.
|
|
343
|
+
|
|
344
|
+
<Example {...elevationTheme} visual="none" />
|
|
345
|
+
|
|
346
|
+
<ElevationLadder
|
|
347
|
+
{...elevationTheme}
|
|
348
|
+
surface="surface"
|
|
349
|
+
levels={['elevation-1', 'elevation-2', 'elevation-3']}
|
|
350
|
+
schemes={['dark', 'light']}
|
|
351
|
+
caption="The same three glows in the dark and light schemes. On the dark surface a few percent of white lifts it 3, 5, and 7 tones; on the light surface the same steps take far more opacity."
|
|
352
|
+
/>
|
|
353
|
+
|
|
354
|
+
A glow on a white light-mode surface has nothing to lighten in SDR, so Glaze
|
|
355
|
+
warns, even if you only use the token in dark mode. On a tone-100 surface it
|
|
356
|
+
leaves the glow transparent. A white written as `'#ffffff'` parses a hair
|
|
357
|
+
below tone 100, so its glow pins to opaque white instead, which paints the
|
|
358
|
+
same over it. Give glows a surface with room in every scheme, like the
|
|
359
|
+
tone-92 surface here.
|
|
360
|
+
|
|
361
|
+
[HDR headroom](#hdr-glows) changes nothing below white. This ladder tops out
|
|
362
|
+
at tone 99.4 (light), so it resolves the same with or without it. Only a glow
|
|
363
|
+
whose step passes white, like one on that white surface, gets an HDR value.
|
|
364
|
+
|
|
365
|
+
## Tinted pigment
|
|
366
|
+
|
|
367
|
+
The pigment is pure black or pure white by default, so the surface keeps its
|
|
368
|
+
own hue under the overlay and the output stays neutral. A pigment tinted in
|
|
369
|
+
the surface's hue ([Josh Comeau's shadow advice](https://www.joshwcomeau.com/css/designing-shadows))
|
|
370
|
+
is opt-in through tuning:
|
|
371
|
+
|
|
372
|
+
- **`reach`** sets where the pigment sits: the fraction of the way from the
|
|
373
|
+
surface's tone to black (shadow) or white (glow). `1` is pure black or
|
|
374
|
+
white, the most reach; lower values let a tint show at the cost of reach.
|
|
375
|
+
- **`tint`** is the pigment's saturation, in the surface's hue. It shows only
|
|
376
|
+
with `reach` below 1, since pure black and white have no hue.
|
|
377
|
+
- **`hueBleed`** pulls the pigment's hue from the surface's hue toward the
|
|
378
|
+
object's (`fg`). It needs `fg`, a `tint`, and `reach` below 1 to show.
|
|
379
|
+
- **`objectInfluence`** is the `β` in [object influence](#object-influence).
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
theme.colors({
|
|
383
|
+
'shadow-tinted': {
|
|
384
|
+
type: 'shadow',
|
|
385
|
+
bg: 'surface',
|
|
386
|
+
intensity: 8,
|
|
387
|
+
tuning: { tint: 0.3, reach: 0.85 },
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
glaze.configure({
|
|
392
|
+
shadowTuning: { objectInfluence: 0.4 },
|
|
393
|
+
glowTuning: { objectInfluence: 0.6 },
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`shadowTuning` and `glowTuning` set the tuning globally (or per theme or token
|
|
398
|
+
through the config override); a per-color `tuning` merges over them field by
|
|
399
|
+
field. Both take these `LightingTuning` fields, and `glowTuning` adds
|
|
400
|
+
[`headroom`](#headroom); defaults are in
|
|
401
|
+
[Color definitions](./reference/color-definitions.md#lightingtuning) and
|
|
402
|
+
[Configuration](./reference/configuration.md#glazeconfig).
|
|
403
|
+
|
|
404
|
+
<TintedPigment
|
|
405
|
+
hue={60}
|
|
406
|
+
saturation={70}
|
|
407
|
+
surfaceSaturation={0.3}
|
|
408
|
+
tones={[97, 50, 12]}
|
|
409
|
+
intensity={8}
|
|
410
|
+
object={{ hue: 200, tone: 50 }}
|
|
411
|
+
columns={[
|
|
412
|
+
{ label: 'neutral (default)', tuning: {} },
|
|
413
|
+
{ label: 'tint 0.3 · reach 0.85', tuning: { tint: 0.3, reach: 0.85 } },
|
|
414
|
+
{
|
|
415
|
+
label: '+ hueBleed 0.6',
|
|
416
|
+
tuning: { tint: 0.3, reach: 0.85, hueBleed: 0.6, objectInfluence: 0 },
|
|
417
|
+
withObject: true,
|
|
418
|
+
},
|
|
419
|
+
]}
|
|
420
|
+
caption="An 8-tone shadow on warm surfaces. The dot is the pigment at full opacity. The hueBleed column points fg at a hue-200 object and sets objectInfluence to 0, so only the hue changes."
|
|
421
|
+
/>
|
|
422
|
+
|
|
423
|
+
The step is exact in every column. On light and mid surfaces a tint barely
|
|
424
|
+
changes the composite; on dark surfaces it costs reach and opacity. On the
|
|
425
|
+
figure's tone-12 surface, black reaches about 12 tones and needs α 0.545 for
|
|
426
|
+
the 8-tone shadow; the `tint: 0.3, reach: 0.85` pigment reaches about 10 tones
|
|
427
|
+
and needs α 0.706.
|
|
428
|
+
|
|
429
|
+
## Unreachable steps
|
|
430
|
+
|
|
431
|
+
Every surface has limited room: a shadow cannot go below black, and a glow
|
|
432
|
+
cannot go above white. When the step is out of reach (a glow on white, a deep
|
|
433
|
+
shadow on near-black), Glaze pins the overlay to the pigment's strongest
|
|
434
|
+
composite (full opacity, or fully transparent when the pigment cannot move the
|
|
435
|
+
surface at all) and emits a deduplicated `console.warn`. A 6-tone glow on a
|
|
436
|
+
tone-100 `auto` surface logs:
|
|
437
|
+
|
|
438
|
+
```text
|
|
439
|
+
glaze: glow "surface-highlight" cannot reach a 6.0-tone step in light
|
|
440
|
+
scheme (reached 0.0 at full strength).
|
|
441
|
+
Lower the intensity, move the surface away from white, or raise a
|
|
442
|
+
tuned-down `reach`.
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
<Unreachable
|
|
446
|
+
cases={[
|
|
447
|
+
{ kind: 'glow', tone: 100, intensity: 6 },
|
|
448
|
+
{ kind: 'glow', tone: 97, intensity: 8 },
|
|
449
|
+
{ kind: 'shadow', tone: 8, intensity: 12 },
|
|
450
|
+
]}
|
|
451
|
+
caption="The big number is the step reached; asked is the step requested. A glow on white stays transparent; the other two pin to full-opacity white and black."
|
|
452
|
+
/>
|
|
453
|
+
|
|
454
|
+
Every shadow and glow resolves in all four schemes on every resolve, whatever
|
|
455
|
+
`modes` you emit and wherever you use the token, so the warning fires even for
|
|
456
|
+
a scheme you never show. Not using a token in a scheme only keeps the pinned
|
|
457
|
+
value off the screen.
|
|
458
|
+
|
|
459
|
+
To fix it, lower `intensity`, or give the overlay a surface with room in
|
|
460
|
+
every scheme. A tuned-down `reach` also costs room; raise it back toward 1.
|
|
461
|
+
Otherwise, expect the warning. Watch dark high contrast in particular: it
|
|
462
|
+
bypasses the tone window, so a page that is white in light mode becomes black
|
|
463
|
+
there and every shadow on it pins and warns. A tone-97 page sits at tone 3,
|
|
464
|
+
which leaves room for less than 3 tones.
|
|
465
|
+
|
|
466
|
+
With [HDR headroom](#hdr-glows), a glow whose target passes white gets an HDR
|
|
467
|
+
value that lands the step, and this warning no longer fires for it. Only the
|
|
468
|
+
headroom cap can leave such a step short, and that logs its own warning. A
|
|
469
|
+
30-tone glow on white with 1 stop of headroom:
|
|
470
|
+
|
|
471
|
+
```text
|
|
472
|
+
glaze: glow "surface-flare" cannot reach a 30.0-tone step in light
|
|
473
|
+
scheme even with 1 stop of headroom (reached 22.0).
|
|
474
|
+
Lower the intensity or raise `headroom`.
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
At the 2-stop maximum the second sentence reads "Lower the intensity;
|
|
478
|
+
`headroom` is already at its 2-stop maximum." A glow without an HDR value
|
|
479
|
+
keeps the SDR warning above.
|
|
480
|
+
|
|
481
|
+
## HDR glows
|
|
482
|
+
|
|
483
|
+
A glow can't go past white in SDR, but an HDR display can show brighter than
|
|
484
|
+
white. Set `glowTuning.headroom`, and a glow whose step would pass white also
|
|
485
|
+
resolves an HDR value: an opaque exposure of what SDR already paints there.
|
|
486
|
+
The SDR value doesn't change, and exporters add the HDR one only when you
|
|
487
|
+
[opt in](#output-is-opt-in).
|
|
488
|
+
|
|
489
|
+
### When a glow gets an HDR value
|
|
490
|
+
|
|
491
|
+
[Tone runs on past 100](./okhst.mdx#tone-above-100) on the same curve: one
|
|
492
|
+
stop above SDR white (`Y` 2) is tone 121.98, two stops (`Y` 4) tone 144.34,
|
|
493
|
+
and 12 tones past white is the same `Y + 0.05` ratio as a 12-tone step
|
|
494
|
+
anywhere. WCAG and APCA end at white, so Glaze makes no contrast claim above
|
|
495
|
+
100. A glow can't be a `bg`, `fg`, `base`, or mix input, so an HDR value never
|
|
496
|
+
reaches the contrast solver.
|
|
497
|
+
|
|
498
|
+
A glow gets an HDR value when three things hold: its `headroom` is above 0,
|
|
499
|
+
its target tone (the surface's luminance tone plus `intensity × k`) passes
|
|
500
|
+
100, and SDR is pinned at its brightest. That means the pigment at full
|
|
501
|
+
opacity, or a white surface (tone 100) at opacity 0, since no pigment can lift
|
|
502
|
+
it. Below white nothing changes, dark-scheme glows included. A pigment that
|
|
503
|
+
can't lift a non-white surface at all (`reach: 0` on a gray surface, say)
|
|
504
|
+
paints nothing in SDR, gets no HDR value, and keeps the SDR warning. On a
|
|
505
|
+
chromatic surface a `reach: 0` pigment, a gray at the surface's tone, can be
|
|
506
|
+
slightly brighter than the surface: it then pins at full opacity and, past
|
|
507
|
+
white, gets an HDR value like any pinned pigment.
|
|
508
|
+
|
|
509
|
+
The resolved variant gains `hdr: { h, s, t, exposure }`. The base (`h`, `s`,
|
|
510
|
+
`t`) is the opaque color SDR paints, with a standard (never pastel) OKHSL
|
|
511
|
+
saturation. The HDR value is that base times `2^exposure`, the gain that lands
|
|
512
|
+
the target tone exactly; `variant.t` itself stays at most 1. The value is
|
|
513
|
+
opaque on purpose: it stays exact whichever way a browser composites, and it
|
|
514
|
+
maps one to one onto native exposure APIs.
|
|
515
|
+
|
|
516
|
+
### Headroom
|
|
517
|
+
|
|
518
|
+
`headroom` counts stops above SDR white, the doublings of its luminance. It
|
|
519
|
+
defaults to 0 (no HDR anywhere) and must be a finite number ≥ 0, or Glaze
|
|
520
|
+
throws. Set it globally (`glaze.configure({ glowTuning: { headroom: 1 } })`),
|
|
521
|
+
in a theme's config override, on one glow's `tuning`, or on
|
|
522
|
+
`glaze.glow()`. A glow's `tuning` merges over `glowTuning` field by field. A
|
|
523
|
+
theme's own `glowTuning` replaces the global one whole, and so does a later
|
|
524
|
+
`glaze.configure({ glowTuning })`, so pass every field you want kept:
|
|
525
|
+
`glaze.configure({ glowTuning: { objectInfluence: 0.6, headroom: 1 } })`. A
|
|
526
|
+
`headroom` on a shadow throws: nothing goes below black.
|
|
527
|
+
|
|
528
|
+
Values above 2 clamp to 2. The warning fires once per field and value, and
|
|
529
|
+
only after the whole input has validated:
|
|
530
|
+
|
|
531
|
+
```text
|
|
532
|
+
glaze: glowTuning.headroom 4 is above the 2-stop maximum; using 2.
|
|
533
|
+
Headroom counts stops (doublings of SDR white's luminance), not a
|
|
534
|
+
ratio: 1 stop is 2× white, 2 stops 4×.
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
The gain is capped per channel, because displays clip per channel: no channel
|
|
538
|
+
of the HDR value passes `2^headroom`. On a neutral glow that caps luminance: a
|
|
539
|
+
30-tone glow on white with 1 stop of headroom ends at
|
|
540
|
+
`color(srgb-linear 2 2 2)`, tone 121.98, and warns (see
|
|
541
|
+
[Unreachable steps](#unreachable-steps)). A saturated base hits the cap on its
|
|
542
|
+
brightest channel first, so its HDR tone can stay below 100.
|
|
543
|
+
|
|
544
|
+
A display shows only as much as its current headroom allows and clips the
|
|
545
|
+
rest, and a page can't read that number. Apple quotes about 2× (1 stop) for
|
|
546
|
+
ordinary Mac and iPad displays, so 1 stop is a sensible ceiling for UI.
|
|
547
|
+
|
|
548
|
+
### Worked example
|
|
549
|
+
|
|
550
|
+
Three 12-tone glows: one on a white `auto` surface, one on a light gray, and
|
|
551
|
+
one on a dark gray.
|
|
552
|
+
|
|
553
|
+
<Example
|
|
554
|
+
{...hdrTheme}
|
|
555
|
+
visual="none"
|
|
556
|
+
after={
|
|
557
|
+
"const glow = theme.resolve().get('surface-glow')!.light;\n" +
|
|
558
|
+
'glaze.formatHdr(glow);\n' +
|
|
559
|
+
"// → 'color(srgb-linear 1.4631 1.4631 1.4631)'"
|
|
560
|
+
}
|
|
561
|
+
/>
|
|
562
|
+
|
|
563
|
+
<HdrGlows
|
|
564
|
+
{...hdrTheme}
|
|
565
|
+
glows={['surface-glow', 'near-glow', 'inverse-glow']}
|
|
566
|
+
caption="Each glow over its surface, in your current scheme. The patch is the SDR result this page paints: the step it reached, the step asked, and its opacity. Below it: the HDR value's tone, its exposure, and its srgb-linear channels, or the target tone that stayed below white. This site renders SDR only."
|
|
567
|
+
/>
|
|
568
|
+
|
|
569
|
+
The same numbers in every scheme:
|
|
570
|
+
|
|
571
|
+
| Glow | SDR | HDR |
|
|
572
|
+
| --- | --- | --- |
|
|
573
|
+
| `surface-glow`, light and light HC | White at α 0 | Linear 1.4631 per channel, 0.549 stops, tone 112 |
|
|
574
|
+
| `surface-glow`, dark and dark HC | White at α 0.1237 (α 0.1603 in dark HC) | None: the dark surface has room |
|
|
575
|
+
| `near-glow`, all four | Opaque white, 5 of 12 tones | Linear 1.2494 per channel, 0.321 stops, tone 107 |
|
|
576
|
+
| `inverse-glow`, all four | White at α 0.1155 | None: the target is tone 24 |
|
|
577
|
+
|
|
578
|
+
`near-glow` shows the split: SDR pins it to opaque white 7 tones short, and
|
|
579
|
+
its HDR value lands those 7. In dark, `surface` sits at tone 9.25 (luminance
|
|
580
|
+
tone 8.5, so the glow's target is tone 20.5), and `surface-glow` is an
|
|
581
|
+
ordinary translucent glow there.
|
|
582
|
+
|
|
583
|
+
### Crossing white
|
|
584
|
+
|
|
585
|
+
A full-reach glow meets SDR at the boundary. On a tone-95 surface a 4.9-tone
|
|
586
|
+
glow is white at α 0.9794 and a 5-tone glow is opaque white, both without an
|
|
587
|
+
HDR value. At 5.1 tones it adds `color(srgb-linear 1.0032 1.0032 1.0032)`,
|
|
588
|
+
0.0046 stops.
|
|
589
|
+
|
|
590
|
+
A glow with `reach` below 1 doesn't meet it. Its pigment sits below white, so
|
|
591
|
+
it stays short and warns until its target passes 100, then its HDR value lands
|
|
592
|
+
the whole step at once. A `reach: 0.5` glow on a tone-90 surface paints
|
|
593
|
+
tone 95 for any step from 5 to 10. Past 10, its HDR value jumps straight to
|
|
594
|
+
tone 100 (0.2315 stops over the pigment). Keep `reach` at 1 on a glow that
|
|
595
|
+
may cross white.
|
|
596
|
+
|
|
597
|
+
### Output is opt-in
|
|
598
|
+
|
|
599
|
+
Resolving fills in `variant.hdr` whenever headroom allows, and
|
|
600
|
+
`glaze.formatHdr(variant)` writes it as `color(srgb-linear R G B)`. Exporters
|
|
601
|
+
leave it out until you turn on `modes.hdr`: globally with
|
|
602
|
+
`glaze.configure({ modes: { hdr: true } })`, or per call (`css()` takes
|
|
603
|
+
`hdr: true`). Until then every export is byte-identical, headroom or not.
|
|
604
|
+
|
|
605
|
+
HDR strings are always `color(srgb-linear …)`, whatever `format` you pass:
|
|
606
|
+
CSS clamps `oklch()` lightness at 1, and `rgb()` / `hsl()` can't go past
|
|
607
|
+
white. How each format carries them, and how to wire the CSS, is in
|
|
608
|
+
[HDR output](./exporting.mdx#hdr-output).
|
|
609
|
+
|
|
610
|
+
### Browsers and native apps
|
|
611
|
+
|
|
612
|
+
As of November 2026, no browser reliably paints a CSS color brighter than
|
|
613
|
+
white:
|
|
614
|
+
|
|
615
|
+
| Engine or spec | What `color(srgb-linear 1.4631 1.4631 1.4631)` paints |
|
|
616
|
+
| --- | --- |
|
|
617
|
+
| CSS Color 4 | SDR white. Values past 1 are valid but gamut-mapped into range for display. |
|
|
618
|
+
| Chrome, Edge | Brighter than white only when it shares a composited layer with HDR image content; SDR white elsewhere. The CSS Working Group intends to remove that path. |
|
|
619
|
+
| Safari | SDR white. WebKit clamps extended colors on purpose, though `dynamic-range: high` matches on HDR-capable devices. |
|
|
620
|
+
| Firefox | SDR white. No CSS HDR. |
|
|
621
|
+
| CSS Color HDR (draft) | Defines `color-hdr()`, which blends colors given at different headroom stops (in practice an SDR and an HDR one), weighted by the display's headroom. No engine implements it. |
|
|
622
|
+
|
|
623
|
+
`@media (dynamic-range: high)` reports what the device can do, not that HDR is
|
|
624
|
+
on: Low Power Mode and the brightness setting change that. And the CSS
|
|
625
|
+
Working Group's direction keeps plain colors such as these in SDR unless they
|
|
626
|
+
opt in through `color-hdr()`, which Glaze doesn't emit yet. So don't count on
|
|
627
|
+
the CSS output to paint past white, today or under the planned standard.
|
|
628
|
+
|
|
629
|
+
Native apps can draw these colors today, and the JSON and DTCG exports carry
|
|
630
|
+
what they take. UIKit and AppKit (iOS and macOS 26) take the base plus
|
|
631
|
+
`exposure` in stops (`UIColor(red:green:blue:alpha:exposure:)` and its
|
|
632
|
+
`NSColor` twin); so does SwiftUI (`exposureAdjust(_:)`). Android takes the
|
|
633
|
+
linear channels as a `LINEAR_EXTENDED_SRGB` color in an HDR window.
|
|
634
|
+
[DTCG and native apps](./exporting.mdx#dtcg-and-native-apps) maps each field.
|
|
635
|
+
|
|
636
|
+
### Fills, not overlays
|
|
637
|
+
|
|
638
|
+
Where the HDR value clamps, which today is most places, it paints an opaque
|
|
639
|
+
color: white, for a neutral glow. That matches SDR wherever SDR is already
|
|
640
|
+
opaque (`near-glow`). On a tone-100 surface it doesn't: SDR is transparent
|
|
641
|
+
there, and the clamped value covers anything drawn between the surface and the
|
|
642
|
+
glow.
|
|
643
|
+
Use HDR glows for fills (a card's or button's own background), edge
|
|
644
|
+
highlights, and `box-shadow` colors, never for a state layer drawn over a
|
|
645
|
+
label.
|
|
646
|
+
|
|
647
|
+
### Power and accessibility
|
|
648
|
+
|
|
649
|
+
- HDR is an enhancement. The design must work in SDR, and it does: the SDR
|
|
650
|
+
value is what every other display already shows.
|
|
651
|
+
- Don't animate or flash HDR values. A pulsing glow past white is a large
|
|
652
|
+
luminance swing, and HDR flash thresholds now appear in the WCAG 2.3.1
|
|
653
|
+
Understanding draft and ITU-R BT.1702-3.
|
|
654
|
+
- Brighter-than-white pixels cost power, and systems fall back to SDR to save
|
|
655
|
+
it (iOS Low Power Mode does). Keep headroom low and HDR areas small.
|
|
656
|
+
|
|
657
|
+
## High contrast
|
|
658
|
+
|
|
659
|
+
`intensity` accepts a `[normal, highContrast]` pair, like other numeric
|
|
660
|
+
fields:
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
theme.colors({
|
|
664
|
+
'card-shadow': {
|
|
665
|
+
type: 'shadow',
|
|
666
|
+
bg: 'surface',
|
|
667
|
+
fg: 'card',
|
|
668
|
+
intensity: [8, 14],
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
With a manual contrast level, the pair is interpolated like every other pair.
|
|
674
|
+
See [high-contrast pairs and the contrast level](./reference/contrast.md#manual-contrast-level).
|
|
675
|
+
|
|
676
|
+
## Fixed opacity
|
|
677
|
+
|
|
678
|
+
For one fixed-alpha color with no opacity solve, such as a modal scrim, set
|
|
679
|
+
`opacity` on a regular color:
|
|
680
|
+
|
|
681
|
+
```ts
|
|
682
|
+
theme.colors({
|
|
683
|
+
scrim: { tone: 0, saturation: 0, mode: 'static', opacity: 0.5 },
|
|
684
|
+
});
|
|
685
|
+
// scrim → 'oklch(0 0 0 / 0.5)' in every scheme
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
`opacity` is applied after the color resolves, so its tone still adapts like
|
|
689
|
+
any regular color. Without `mode: 'static'`, `tone: 0` is lifted by the light
|
|
690
|
+
tone window and inverted into a light overlay in dark mode. A fixed alpha
|
|
691
|
+
gives a different step on every surface (an 8% black layer darkens white by
|
|
692
|
+
about 6 tones and a tone-10 surface by about 1), which is what shadows and
|
|
693
|
+
glows exist to avoid. For a translucent color with a contrast floor, use a
|
|
694
|
+
[transparent mix](./mixes.mdx#transparent-mix).
|
|
695
|
+
|
|
696
|
+
## Outside a theme
|
|
697
|
+
|
|
698
|
+
`glaze.shadow()` and `glaze.glow()` compute an overlay without a theme. `bg`
|
|
699
|
+
and `fg` accept any color value Glaze parses (hex, `rgb()`, `okhsl()`, value
|
|
700
|
+
objects, …):
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
const shade = glaze.shadow({ bg: '#ffffff', intensity: 6 });
|
|
704
|
+
// → { h: 0, s: 0, t: 0, alpha: 0.0814… }
|
|
705
|
+
|
|
706
|
+
glaze.format(shade, 'rgb');
|
|
707
|
+
// → 'rgb(0 0 0 / 0.0814)'
|
|
708
|
+
|
|
709
|
+
const lift = glaze.glow({ bg: '#1e1e22', fg: '#3a3a44', intensity: 4 });
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Both accept an optional `tuning` that merges over the global `shadowTuning` /
|
|
713
|
+
`glowTuning`. With a `headroom` there, `glaze.glow()` also returns
|
|
714
|
+
[`hdr`](#hdr-glows) when its step passes white:
|
|
715
|
+
|
|
716
|
+
```ts
|
|
717
|
+
const flare = glaze.glow({
|
|
718
|
+
bg: '#ffffff',
|
|
719
|
+
intensity: 12,
|
|
720
|
+
tuning: { headroom: 1 },
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
glaze.formatHdr(flare);
|
|
724
|
+
// → 'color(srgb-linear 1.4631 1.4631 1.4631)'
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
The full input table is in
|
|
728
|
+
[Utilities](./reference/utilities.md#glazeshadowinput--glazeglowinput).
|
|
729
|
+
|
|
730
|
+
## Coming from 2.x
|
|
731
|
+
|
|
732
|
+
In Glaze 2, `intensity` went through a curve in OKHSL lightness, so the same
|
|
733
|
+
number darkened white, mid, and dark surfaces by very different amounts, and
|
|
734
|
+
`fg` was a contrast reference that gave a white card on a light page almost no
|
|
735
|
+
shadow. 3.0 makes `intensity` an exact tone step, makes `fg` the object, and
|
|
736
|
+
adds `glow`. Upgrade steps are in
|
|
737
|
+
[3.0 — shadows and glows](./migration.md#30--shadows-and-glows).
|