Back to the library
Option set narrowing with a held result
Variant: Held result
The narrowing pass is quick — the rejects dim in a fast 180ms window — and then the single-option state is held for most of the loop before the rack returns. This is the timing to use when the point is the calm after the filter rather than the filter itself, such as a smart-default row that should look settled 80% of the time. The return leg is deliberately slower than the collapse so the reappearing options read as re-offered, not as a glitch.
filterholdchipsstate-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 — held result</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: 5s;
--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);
--_step: calc(0s / 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; color: var(--clay-d); font-weight: 600; }
/* Collapse lands by 8%; the single-option label then owns the loop until 88%. */
@keyframes countMany {
0%, 3% { opacity: 1; }
8%, 88% { opacity: 0; }
97%, 100% { opacity: 1; }
}
@keyframes countFew {
0%, 4% { opacity: 0; }
9%, 86% { opacity: 1; }
96%, 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;
}
/* Out in ~180ms, back over ~600ms: the re-offer is the slower half. */
@keyframes reject {
0%, 2% { transform: scale(1); opacity: 1; }
8%, 86% { transform: scale(0.94); opacity: 0.18; }
98%, 100% { transform: scale(1); opacity: 1; }
}
@keyframes keep {
0%, 2% { transform: scale(1) translateY(0); opacity: 1; }
8%, 86% { transform: scale(1.04) translateY(-1px); opacity: 1; }
98%, 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;
}
@keyframes rail {
0%, 2% { transform: scaleX(1); }
8%, 86% { transform: scaleX(0.24); }
98%, 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;
}
@keyframes dot {
0%, 2% { opacity: 0.35; }
8%, 86% { opacity: 1; }
98%, 100% { opacity: 0.35; }
}
@media (max-width: 380px) {
.card { padding: 12px; gap: 11px; }
.rack { gap: 6px; }
}
@media (prefers-reduced-motion: reduce) {
/* Hold stays long; the transitions become slow crossfades with no scale. */
.chip,
.chip.keep {
animation-duration: calc(var(--t) * 2);
animation-delay: 0s;
}
@keyframes reject {
0%, 2% { transform: none; opacity: 1; }
10%, 86% { transform: none; opacity: 0.3; }
98%, 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); }
}
</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:2">Std</div>
<div class="chip" style="--_i:3">L</div>
<div class="chip" style="--_i:4">XL</div>
<div class="chip" style="--_i:5">Pro</div>
<div class="chip" style="--_i:6">Max</div>
<div class="chip" style="--_i:7">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.