The guidehow this page was made

One field, seven pigments

The page is the specimen: its background is a single colour that travels through seven historical pigments as you scroll. No images, no canvas, no WebGL.

The concept

Every colour was once a place, a price and an obsession. PIGMENT tells seven of those stories as a museum catalogue: colossal serif specimen names, tracked-caps labels, specimen cards, and the raw design tokens exposed on the page as content. The exhibit itself is the background. Between chapters the field interpolates in OKLCH from one pigment to the next; inside chapters it rests on a solid plateau.

The engine: three registered channels

The field is one fixed element. Six progress channels, one per transition, are registered with @property so the browser knows they are numbers and can interpolate them. Each is driven from 0 to 1 by the scroll position of its own vault (the text-free strip between two chapters), through a named view-timeline:

@property --t3 { syntax: "<number>"; inherits: false; initial-value: 0; }

body    { timeline-scope: --vault-1, ... , --vault-6; }
.vault-3 { view-timeline: --vault-3 block; }

@keyframes prog-3 { from { --t3: 0; } to { --t3: 1; } }

.pigment-field {
  animation-name: prog-1, ... , prog-6;
  animation-duration: auto;
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: --vault-1, ... , --vault-6;
  animation-range: contain 12% contain 88%;
}

animation-fill-mode: both means each channel reads 0 before its vault and 1 after it, so at any scroll position the six numbers describe exactly how far along the pigment path you are. They compose into the three OKLCH channels as delta chains, and the colour is mixed perceptually byoklch() itself:

.pigment-field {
  --bg-l: calc(0.205 + 0.175*var(--t1) - 0.08*var(--t2) + 0.4*var(--t3)
              + 0.14*var(--t4) - 0.34*var(--t5) - 0.17*var(--t6));
  /* --bg-c and --bg-h: same idea */
  background-color: oklch(var(--bg-l) var(--bg-c) var(--bg-h));
}

Because every transition animates its own registered property, the six animations never fight over a value. No JavaScript anywhere in the engine.

One deliberate subtlety: Tyrian purple is keyframed with hue -20rather than the equivalent 340, so the numeric interpolation from vermilion (hue 32) travels down through the reds instead of up through green.

The contrast engineering

A page whose background can be any colour on a continuous path must still keep every word at WCAG AA. Three decisions make that a guarantee rather than a hope:

  1. Chapters are ordered so neighbours share an ink where possible. The ink sequence is light, light, light, dark, dark, light, light: only two flips exist (indigo to verdigris, Indian yellow to vermilion).
  2. Text never sits on a moving field. Chapters keep their own solid pigment background in every mode; only the text-free 170vh vaults are windows onto the animated field. The contain range of a 170vh element is, by spec, exactly the scroll span where the viewport is fully inside it, so every animated frame shows vault interior only. Legibility is therefore independent of animation state, at any viewport size.
  3. Every pairing is computed, not eyeballed. scripts/contrast.mjs converts each OKLCH token to sRGB, computes WCAG relative luminance, and prints ratio tables for the plateaus and for the full interpolation path at eighth steps. The mid-transition dead zones (where neither ink reaches 4.5:1, for example indigo to verdigris around t 0.5 to 0.75) are exactly why rule 2 exists.
The locked pairings, from scripts/contrast.mjs
SpecimenFieldInkContrast
Bone Blackoklch(0.205 0.012 265)light15.72 : 1
Lapis Lazulioklch(0.38 0.155 264)light9.11 : 1
Indigooklch(0.3 0.085 274)light12.17 : 1
Verdigrisoklch(0.7 0.105 182)dark6.90 : 1
Indian Yellowoklch(0.84 0.145 88)dark10.69 : 1
Vermilionoklch(0.5 0.185 32)light5.78 : 1
Tyrian Purpleoklch(0.33 0.105 340)light11.28 : 1

The fallback is not an afterthought

The static presentation is the default stylesheet: every chapter on its solid pigment, every vault a fixed linear-gradient(in oklch, A, B) whose shorter-hue path matches the animated route. The animated field, the reveals and the progress spine are layered on top only inside@media (prefers-reduced-motion: no-preference) and@supports (animation-timeline: view()). Browsers without scroll-driven animations, and readers who prefer reduced motion, get the same palette, the same copy, the same catalogue, complete.

Three iteration passes

The build went through three mandatory passes before anyone saw it, each with screenshots at every chapter and mid-transition, at two viewports. Pass one hunts defects: contrast at transition midpoints, blend-mode legibility, fallback quality. Pass two complexifies: richer specimen cards, marginal annotations, the ghost numerals, the swatch strips. Pass three restores cohesion and removes what pass two overdid. The log lives in ITERATIONS.md in the repository.

The complete creative process is reproducible: the design brief and the development architecture live as reusable prompts in the repository'sprompts/ folder.