Ambient — drifting colour field
Variant: Colour field
The soft-gradient background, built so it cannot be caught repeating. Each wash is an oversized square with its blob sitting off-centre, rotated about the panel’s middle — a rotation is the only closed path with no waypoints to ease through, so there is no stop-start anywhere in the drift and the whole thing costs one composited property per layer. The two run counter at 19s and 23s, and because those are coprime the pair only returns to its opening arrangement once every 437 seconds. An ambient loop is judged on whether a reader can tell you where it was ten seconds ago, and this one is running under copy that already has their attention.
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ambient — drifting colour field</title>
<style>
:root {
/* Neutrals — fixed, never remixed. */
--ivory: #faf9f5;
--paper: #ffffff;
--oat: #e3dacc;
--slate: #141413;
/* Knobs — the remix API. A knob exists iff declared here and used below. */
--hue: 38.8;
--lift: 0;
--chroma: 1;
--round: 1;
--speed: 1;
/* Accents — derived from the knobs with the family's offsets baked in,
so one hue roll moves them together. Defaults reproduce the original hex. */
--clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
--clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
--olive: oklch(
calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
);
--rust: oklch(
calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
);
/* Clock — 19s and 23s are coprime, so the two washes only return to
their opening arrangement once every 437 seconds. Nobody watches a
background for seven minutes, which is the whole reason for the odd
numbers: the loop exists but cannot be caught. */
--cycle: 19s;
--t: calc(var(--cycle) / var(--speed));
--_second: calc(23s / var(--speed));
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
.panel {
position: relative;
overflow: hidden;
width: min(392px, calc(100% - 26px));
height: clamp(150px, 46vw, 214px);
border: 1px solid var(--oat);
border-radius: calc(16px * var(--round));
background: var(--paper);
}
/* Each wash is a square far wider than the panel with its blob sitting
off-centre, so rotating it about the panel's middle carries the colour
round a perfect circle. A rotation is the only closed path with no
waypoints in it — nothing to ease through, no corner to soften, and one
composited property per layer for the whole drift. */
.wash {
position: absolute;
top: 50%;
left: 50%;
width: 178%;
aspect-ratio: 1;
translate: -50% -50%;
animation: turn var(--t) linear infinite;
}
.wash-a {
background: radial-gradient(
30% 30% at 34% 38%,
color-mix(in srgb, var(--clay) 32%, transparent),
transparent 100%
);
}
/* Counter-rotating on the longer clock. Same direction and the pair would
read as one rigid pattern turning rather than two clouds passing. */
.wash-b {
background: radial-gradient(
34% 34% at 68% 62%,
color-mix(in srgb, var(--olive) 26%, transparent),
transparent 100%
);
animation-duration: var(--_second);
animation-direction: reverse;
}
@keyframes turn {
to {
rotate: 1turn;
}
}
/* The copy is not decoration. An ambient field is only ever seen under
something, and contrast against real text at real size is the only
constraint it actually has to meet. */
.copy {
position: absolute;
right: clamp(16px, 4.6vw, 24px);
bottom: clamp(14px, 4.2vw, 22px);
left: clamp(16px, 4.6vw, 24px);
}
.head {
font-size: clamp(15px, 4.4vw, 19px);
font-weight: 600;
letter-spacing: -0.01em;
}
.sub {
margin-top: 3px;
font-size: clamp(11.5px, 3.3vw, 13px);
color: color-mix(in srgb, var(--slate) 55%, transparent);
}
@media (prefers-reduced-motion: reduce) {
/* The travel goes and the field still has to be alive, because a frozen
gradient is indistinguishable from a rendering mistake. The two washes
breathe on the same coprime pair, so the surface still never quite
settles into a state it has held before. */
.wash {
animation-name: soften;
animation-timing-function: ease;
}
@keyframes soften {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
}
</style>
</head>
<body>
<div class="panel">
<div class="wash wash-a" aria-hidden="true"></div>
<div class="wash wash-b" aria-hidden="true"></div>
<div class="copy">
<p class="head">Everything is quiet</p>
<p class="sub">Ambient motion. Nothing here reports state.</p>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.