Ambient — dust motes
Variant: Dust motes
Sixteen motes falling through a shaft of light at sixteen durations that share no simple ratio — 0.71 to 1.63 of the base clock — because a field seeded with random numbers clumps and a field on tidy multiples visibly falls in step. Every lane carries a negative animation-delay, so the panel is already populated on its first frame instead of spending nine seconds filling from the top; an ambient background has no opening beat. Each mote rides a lane the full height of the panel, which is the only reason a percentage travel stays fluid on a 4px dot, and its opacity envelope opens and closes well inside the frame so nothing is ever seen appearing at an edge. Sway runs on a separate period from the fall, or every path would be the same repeating diagonal.
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 — dust motes</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 — one fall at the base rate. Each mote multiplies it by its own
factor between 0.71 and 1.63, and no two of those factors share a
simple ratio, which is what stops the field ever falling in step. */
--cycle: 9s;
--t: calc(var(--cycle) / var(--speed));
--_dot: 4px;
--_sway: 7px;
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}
* {
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);
}
/* Static. The motes need a reason to be catching light, and a shaft that
also moved would be a second animation competing with the first. */
.shaft {
position: absolute;
inset: 0;
background: linear-gradient(
104deg,
transparent 20%,
color-mix(in srgb, var(--oat) 74%, transparent) 52%,
transparent 80%
);
}
/* Each mote hangs off the bottom of a lane the full height of the panel,
so translateY percentages resolve against the panel rather than against
a 4px dot. That is the only reason the travel stays fluid across every
preview size without a single hard-coded distance. */
.lane {
position: absolute;
top: 0;
left: var(--_x);
width: 0;
height: 100%;
transform: translateY(var(--_y));
animation: fall calc(var(--t) * var(--_d)) linear infinite;
/* Negative, so the field is already populated on the first frame. A
positive delay would spend the first nine seconds filling from the
top, and an ambient background has no opening beat. */
animation-delay: calc(var(--t) * var(--_lag) * -1);
}
.lane > i {
position: absolute;
bottom: 0;
left: 0;
width: calc(var(--_dot) * var(--_s));
height: calc(var(--_dot) * var(--_s));
border-radius: 50%;
translate: -50% 0;
/* Big motes are bright and small ones are dim. It is the only depth cue
in the piece, and it costs nothing to run. */
opacity: calc(0.26 + var(--_s) * 0.28);
background: var(--clay-d);
animation: sway calc(var(--t) * var(--_w)) var(--ease-in-out) infinite alternate;
}
/* The envelope opens and closes well inside the frame, so no mote is ever
seen appearing or being cut off at an edge. */
@keyframes fall {
0% {
transform: translateY(-112%);
opacity: 0;
}
11%,
82% {
opacity: 1;
}
100% {
transform: translateY(12%);
opacity: 0;
}
}
/* Sway runs on its own period, unrelated to the fall. Tie the two together
and every mote traces the same repeating diagonal. */
@keyframes sway {
from {
transform: translateX(calc(var(--_sway) * -1));
}
to {
transform: translateX(var(--_sway));
}
}
.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 fall stops and each lane rests at its own --_y, which is why that
offset exists at all — cut the animation without it and sixteen motes
stack up on the bottom edge. They breathe instead, on the same
per-mote periods and the same authored offsets, so the field is still
incoherent rather than blinking as one. */
.lane {
animation: none;
}
.lane > i {
animation-name: glow;
animation-delay: calc(var(--t) * var(--_lag) * -1);
transform: none;
}
@keyframes glow {
0% {
opacity: 0.18;
}
100% {
opacity: 0.5;
}
}
}
</style>
</head>
<body>
<div class="panel">
<div class="shaft" aria-hidden="true"></div>
<div class="field" aria-hidden="true">
<span
class="lane"
style="--_x: 4%; --_y: -12%; --_d: 1; --_lag: 0.03; --_s: 1.3; --_w: 0.61"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 11%; --_y: -71%; --_d: 1.37; --_lag: 0.62; --_s: 0.8; --_w: 0.74"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 15%; --_y: -38%; --_d: 0.83; --_lag: 0.29; --_s: 1.1; --_w: 0.53"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 23%; --_y: -86%; --_d: 1.19; --_lag: 0.81; --_s: 0.7; --_w: 0.67"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 29%; --_y: -22%; --_d: 1.53; --_lag: 0.14; --_s: 1.4; --_w: 0.58"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 33%; --_y: -57%; --_d: 0.94; --_lag: 0.47; --_s: 0.9; --_w: 0.79"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 41%; --_y: -91%; --_d: 1.29; --_lag: 0.93; --_s: 1.2; --_w: 0.64"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 47%; --_y: -33%; --_d: 0.79; --_lag: 0.36; --_s: 0.75; --_w: 0.71"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 51%; --_y: -66%; --_d: 1.11; --_lag: 0.7; --_s: 1.35; --_w: 0.56"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 59%; --_y: -47%; --_d: 1.44; --_lag: 0.21; --_s: 0.85; --_w: 0.77"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 65%; --_y: -78%; --_d: 0.88; --_lag: 0.55; --_s: 1.05; --_w: 0.68"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 69%; --_y: -18%; --_d: 1.63; --_lag: 0.09; --_s: 0.95; --_w: 0.51"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 77%; --_y: -62%; --_d: 0.71; --_lag: 0.74; --_s: 1.25; --_w: 0.66"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 83%; --_y: -41%; --_d: 1.23; --_lag: 0.42; --_s: 0.72; --_w: 0.73"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 87%; --_y: -84%; --_d: 1.05; --_lag: 0.88; --_s: 1.15; --_w: 0.59"
>
<i></i>
</span>
<span
class="lane"
style="--_x: 95%; --_y: -28%; --_d: 0.97; --_lag: 0.26; --_s: 0.88; --_w: 0.63"
>
<i></i>
</span>
</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.