atelier

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 flat values.
  • 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 licence
  • css-color-4: OKLab and OKLCH as CSS colour spaces
  • demofox-interleaved-gradient-noise: the one-line dither
  • wcag-contrast-minimum: the 4.5 to 1 floor the guard keeps
  • wcag-three-flashes: why a band’s one rise and fall is inside the limit
  • wcag-pause-stop-hide: why the continuous form is avoided
  • mdn-drawingbuffercolorspace: the wide-gamut path, left out
  • jcgt-hash-functions: the integer hash, which never ships a sine