Catalog / Technique
Colour field
Use when colour itself is the effect on a page that has design tokens, such as a colour wash on load or a tinted hero that resolves. Covers anchoring on the tokens, OKLab and OKLCH, gamut, settling to an exact colour, dithering, and what the field takes from text contrast.
- Loads as
technique-colour-field- Written for
- An agent, to follow
Colour field
A low-frequency field mapped through a palette, filling an element. The family’s usual form runs forever behind a hero. This skill is for the form that plays once and ends as the page’s own colours, with nothing left running and nothing left on screen.
When to use
- A load effect, a hero or a section intro on a page with tokens, where colour arrives and resolves into the page.
- A transition between two states of a page, as the thing that moves.
Do not use it for:
- A continuous background. That form owes the reader a pause control after five seconds and never has an answer for battery.
- Behind charts, tables or anything read by colour.
- Listings and card grids, where it tints the things being compared.
Rules
- The tokens are the origin. The ground’s lightness and the accent’s hue are the coordinates; chroma, lift and spread are dials on them. A free palette is a second design system.
- Mix in OKLab for ramps; sweep in OKLCH when hue is the dial. A plain mix of sRGB values is the muddy one.
- Fit to gamut by sliding toward the grey of the same lightness. Never clamp channels: it shifts the hue.
- It settles. One number from 1 to 0 scales chroma and lift together, and at 0 the output is the ground token exactly. Add the sweep to the ground’s own Lab; a near-white is rarely neutral.
- End by removing an opaque canvas that already shows the ground. A half-transparent ground over the same ground comes back one step dark; a fade out is not a no-op. A fade in of about 100 ms is fine.
- Dither the encoded output by one step. Always on dark grounds, where one sRGB step is a large relative change. Interleaved gradient noise is one line.
- Render small. A colour field has no detail to lose; a quarter of the pixels each way is a sixteenth of the cost.
- Convert the tokens once, not per pixel. The vertex shader of a
full-screen triangle does it three times a frame and hands the
fragment
flatvalues. - Read tokens through a probe, and check for a missing one. A missing custom property inherits the text colour and the effect runs with ink where it expected an accent. Name the stand-in.
- sRGB unless a token is outside it. Then a P3 buffer, P3 bytes and P3 matrices, and only on a P3 screen. A mismatch can only show mid-effect, never at the end.
- The text at risk is the muted token and the accent link, not the body. Give the recipe the weakest text colour it sits behind and cap the lift from it, or keep the field out of the text.
- A warm accent on a dark ground makes brown. Tune the dark theme separately; try a cooler or wider spread.
- A moving edge needs margins. A front or band starts wholly off one side and ends wholly off the other, bend included.
Decisions
| Question | Choose | Because |
|---|---|---|
| Envelope? | Drain by default | Nothing travels; it is the calmest, and monotone per pixel, so it is not a flash |
| Behind or over? | Behind | Over tints images for the duration; it is for pages whose cards hide a canvas behind them |
| Isoluminant? | No; lift 0 is the dial’s low end | At lift 0 almost no chroma fits on a near-white ground |
| The guard? | On, from the weakest text token | Muted text and accent links drop under 4.5 to 1 at a lift of 0.04 |
| A still? | Never | A frozen tint is a background with a contrast obligation that never ends; a CSS gradient does that for no script |
| Wide gamut? | Not until a token is outside sRGB | Twenty lines and a second pair of matrices for a case that may never occur |
Tuning
| Dial | Start | Range | Low end | High end |
|---|---|---|---|---|
| Chroma (OKLCH) | 0.06 | 0.014 to 0.12 | Tints you have to look for; 0.014 is all every hue reaches on near-white | Candy. Wrong for a page meant to be trusted |
| Lift (OKLab L from the ground) | 0.05 | 0 to 0.1 | Contrast cannot move; almost no chroma fits | Small text starts to fail |
| Hue spread (turns either side) | 0.18 | 0 to 0.5 | One hue, the accent’s | The whole wheel |
| Scale (CSS px per cell) | 350 | 200 to 900 | Several blobs in view | One slow sweep |
| Warp | 0.9 | 0 to 1.5 | Round blobs | Folded ribbons |
| Octaves | 2 | 1 to 3 | Smooth | Wasted on a field this soft |
| Front softness (share of the element) | 0.12 | 0.05 to 0.3 | A visible edge | Indistinguishable from a drain |
| Band width (share of the element) | 0.28 | 0.15 to 0.5 | A thin ribbon | Most of the element at once |
| Render scale | 0.25 | 0.125 to 1 | Cheapest; the dither grain becomes 4 px blocks | Sixteen times the cost for no visible gain |
| Duration | 1100 ms | 500 to 1600 | A flick | A loading screen |
Ranges are from stills on a light stand-in theme. The dark theme’s numbers are not these.
Rest state
Nothing, always: the page in its own tokens. Under reduced motion, forced colours, without WebGL2 or on a software renderer nothing starts. A lost context removes the canvas at once; a half-tinted page is never left.
Cost
- To build: small on a finished harness. The tuning is harder than the build: the dials interact with the theme.
- To run: about 1.6 kB of shader on a 2.3 kB harness. Per pixel it costs more than the contour map as written, which is why it renders at a quarter scale. A second of work, then nothing.
- To the reader: a second of colour behind what they came to read, and a dip in the contrast of the weakest text for that second. The guard bounds the dip.
Checklist
-
npm run check:browser: held at 0.999, every sampled pixel is the ground token to the byte, for every envelope. -
npm run stills, and the frames match the envelope’s name. - The muted text and the accent link stay readable at the first frame, by eye.
- On a dark ground the field is not brown, and the dither grain at a quarter scale is looked at on a real screen.
- Under reduced motion no context is requested.
- Run once in Safari and once in Firefox before it is marked passed.
Recipes
palette-resolve: the one-shot form, with four envelopes and the guard.shader-wash: a single band of one token colour, which is this family’s smallest member.
Sources
ottosson-oklab: the OKLab definition, its matrices and its licencecss-color-4: OKLab and OKLCH as CSS colour spacesdemofox-interleaved-gradient-noise: the one-line ditherwcag-contrast-minimum: the 4.5 to 1 floor the guard keepswcag-three-flashes: why a band’s one rise and fall is inside the limitwcag-pause-stop-hide: why the continuous form is avoidedmdn-drawingbuffercolorspace: the wide-gamut path, left outjcgt-hash-functions: the integer hash, which never ships a sine