Back to the library
Option set narrowing with a left-to-right sweep
Variant: Sweep stagger
The same chip rack collapsing to one recommendation, but the rejects drop out in sequence at 55ms apart so the eye can follow the pass rather than being handed a result. Reach for this when the narrowing itself is the message — a filter being applied, a "we removed the options nobody picks" explainer. The kept chip is excluded from the stagger and lifts on the shared beat, so it reads as the survivor of the sweep rather than another step in it.
filterstaggerchipsstate-changeloop
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>Option set narrowing — sweep stagger</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
(#d97757 / #b85c3e / #788c5d / #b04a3f). */
--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 — every duration in the document runs off --t. */
--cycle: 4s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
/* 55ms between rejects: inside the 30-80ms cascade window, and 7 steps
still land the last chip before the settle. */
--_step: calc(55ms / var(--speed));
--_dim: #8c857a;
}
* { box-sizing: border-box; }
html, body { margin: 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);
padding: 16px;
}
.card {
width: min(400px, 92vw);
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
padding: clamp(14px, 4.5vw, 20px);
display: grid;
gap: clamp(12px, 3.5vw, 16px);
}
.head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.title {
font-size: clamp(12px, 3.4vw, 13.5px);
font-weight: 600;
letter-spacing: 0.01em;
}
.count {
font-size: clamp(11px, 3vw, 12px);
color: var(--_dim);
font-variant-numeric: tabular-nums;
display: inline-grid;
}
.count span {
grid-area: 1 / 1;
text-align: right;
}
.count .many { animation: countMany var(--t) var(--ease-out) infinite; }
.count .few {
animation: countFew var(--t) var(--ease-out) infinite;
/* Label flips only after the last chip has fallen. */
animation-delay: calc(var(--_step) * 7);
color: var(--clay-d);
font-weight: 600;
}
@keyframes countMany {
0%, 14% { opacity: 1; }
26%, 82% { opacity: 0; }
96%, 100% { opacity: 1; }
}
@keyframes countFew {
0%, 16% { opacity: 0; }
30%, 80% { opacity: 1; }
94%, 100% { opacity: 0; }
}
.rack {
display: flex;
flex-wrap: wrap;
gap: clamp(6px, 2vw, 8px);
}
.chip {
--_i: 0;
flex: 0 0 auto;
min-width: clamp(46px, 14vw, 58px);
height: clamp(28px, 8vw, 32px);
border-radius: 999px;
border: 1px solid var(--oat);
background: var(--ivory);
display: grid;
place-items: center;
font-size: clamp(11px, 3vw, 12px);
color: var(--_dim);
transform-origin: 50% 50%;
will-change: transform, opacity;
animation: reject var(--t) var(--ease-in-out) infinite;
animation-delay: calc(var(--_i) * var(--_step));
}
.chip.keep {
border-color: var(--clay);
color: var(--paper);
background: var(--clay);
font-weight: 600;
animation-name: keep;
animation-delay: 0s; /* the survivor sits outside the cascade */
}
@keyframes reject {
0%, 12% { transform: scale(1); opacity: 1; }
30%, 78% { transform: scale(0.94); opacity: 0.18; }
96%, 100% { transform: scale(1); opacity: 1; }
}
@keyframes keep {
0%, 12% { transform: scale(1) translateY(0); opacity: 1; }
30%, 78% { transform: scale(1.04) translateY(-1px); opacity: 1; }
96%, 100% { transform: scale(1) translateY(0); opacity: 1; }
}
.rail {
height: 3px;
border-radius: 999px;
background: var(--oat);
overflow: hidden;
}
.rail i {
display: block;
height: 100%;
border-radius: 999px;
background: var(--olive);
transform-origin: 0 50%;
transform: scaleX(0.24);
animation: rail var(--t) var(--ease-out) infinite;
animation-delay: calc(var(--_step) * 3);
}
@keyframes rail {
0%, 12% { transform: scaleX(1); }
30%, 78% { transform: scaleX(0.24); }
96%, 100% { transform: scaleX(1); }
}
.foot {
font-size: clamp(10.5px, 2.9vw, 11.5px);
color: var(--_dim);
display: flex;
align-items: center;
gap: 6px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--rust);
animation: dot var(--t) var(--ease-out) infinite;
animation-delay: calc(var(--_step) * 7);
}
@keyframes dot {
0%, 12% { opacity: 0.35; }
30%, 78% { opacity: 1; }
96%, 100% { opacity: 0.35; }
}
@media (max-width: 380px) {
.card { padding: 12px; gap: 11px; }
.rack { gap: 6px; }
}
@media (prefers-reduced-motion: reduce) {
/* The cascade collapses to one shared fade; the loop keeps running at half pace. */
.chip,
.chip.keep {
animation-duration: calc(var(--t) * 2);
animation-delay: 0s;
}
@keyframes reject {
0%, 12% { transform: none; opacity: 1; }
30%, 78% { transform: none; opacity: 0.3; }
96%, 100% { transform: none; opacity: 1; }
}
@keyframes keep {
0%, 100% { transform: none; opacity: 1; }
}
.rail i,
.count .many,
.count .few,
.dot {
animation-duration: calc(var(--t) * 2);
animation-delay: 0s;
}
}
</style>
</head>
<body>
<div class="card">
<div class="head">
<div class="title">Plan</div>
<div class="count">
<span class="many">24 shown</span>
<span class="few">1 suggested</span>
</div>
</div>
<div class="rack">
<div class="chip" style="--_i:0">S</div>
<div class="chip" style="--_i:1">M</div>
<div class="chip keep" style="--_i:0">Std</div>
<div class="chip" style="--_i:2">L</div>
<div class="chip" style="--_i:3">XL</div>
<div class="chip" style="--_i:4">Pro</div>
<div class="chip" style="--_i:5">Max</div>
<div class="chip" style="--_i:6">Team</div>
</div>
<div class="rail"><i></i></div>
<div class="foot"><span class="dot"></span>Filtered to the best fit</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.