Back to the library
Verdict thumb, snap
Variant: Snap
The same Hold/Sell thumb on a strong ease-out curve. It covers most of the distance in the first few frames and spends the remaining time easing into place. Use it when the control has to feel instant, such as a filter the user flips often or a setting toggled from the keyboard. The motion is front-loaded, so the new state is readable within about 80ms of the 280ms slide.
segmented-controltogglestate-changeease-out
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>Verdict thumb — snap</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; /* include ONLY if at least one border-radius is wrapped below */
--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: 3.2s; /* the authored master loop */
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* include only if used */
--_pad: 7px;
}
* { box-sizing: border-box; margin: 0; }
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
color: var(--slate);
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
.card {
width: min(340px, 92vw);
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(16px * var(--round));
padding: clamp(16px, 5vw, 22px);
display: grid;
gap: clamp(10px, 3vw, 14px);
}
.meta {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
font-size: clamp(12px, 3.4vw, 13px);
}
.meta b { font-size: clamp(14px, 4vw, 16px); font-weight: 600; letter-spacing: 0.02em; }
.meta span { opacity: 0.6; white-space: nowrap; }
.seg {
position: relative;
height: 46px;
border-radius: 999px;
background: var(--ivory);
border: 1px solid var(--oat);
}
/* Padding equals the worst-case overshoot of the springy sibling, so all four share geometry. */
.rail { position: absolute; inset: var(--_pad); }
.labels {
position: absolute;
inset: 0;
display: flex;
font-size: clamp(13px, 3.8vw, 15px);
font-weight: 600;
}
.labels span { flex: 1; display: grid; place-items: center; }
.base { opacity: 0.55; }
.thumb {
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
border-radius: 999px;
background: var(--clay);
box-shadow: 0 2px 6px color-mix(in oklch, var(--clay-d) 35%, transparent);
overflow: hidden;
will-change: transform;
animation: slide var(--t) var(--ease-out) infinite;
}
/* The inked copy spans the whole rail and counter-translates on the same curve,
so white text is revealed only where the thumb sits — no colour animation needed. */
.thumb .labels {
inset: 0 auto 0 0;
width: 200%;
color: var(--paper);
animation: counter var(--t) var(--ease-out) infinite;
}
.thumb.alt { display: none; }
/* 280ms out to Sell, 220ms back to Hold; Hold also keeps the longer dwell. */
@keyframes slide {
0%, 40% { transform: translateX(0); }
48.75%, 70% { transform: translateX(100%); }
76.875%, 100% { transform: translateX(0); }
}
@keyframes counter {
0%, 40% { transform: translateX(0); }
48.75%, 70% { transform: translateX(-50%); }
76.875%, 100% { transform: translateX(0); }
}
@keyframes rm-hold {
0%, 40% { opacity: 1; }
48.75%, 70% { opacity: 0; }
76.875%, 100% { opacity: 1; }
}
@keyframes rm-sell {
0%, 40% { opacity: 0; }
48.75%, 70% { opacity: 1; }
76.875%, 100% { opacity: 0; }
}
@media (max-width: 380px) {
.card { padding: 16px; }
.seg { height: 44px; }
}
@media (prefers-reduced-motion: reduce) {
.thumb, .thumb .labels { animation: none; }
.thumb.main { animation: rm-hold calc(var(--t) * 2) var(--ease-out) infinite; }
.thumb.alt {
display: block;
transform: translateX(100%);
animation: rm-sell calc(var(--t) * 2) var(--ease-out) infinite;
}
.thumb.alt .labels { transform: translateX(-50%); }
}
</style>
</head>
<body>
<div class="card">
<div class="meta"><b>NVDA</b><span>120 sh · −3.1% today</span></div>
<div class="seg" role="radiogroup" aria-label="Next action">
<div class="rail">
<div class="labels base"><span>Hold</span><span>Sell</span></div>
<div class="thumb main" aria-hidden="true"><div class="labels"><span>Hold</span><span>Sell</span></div></div>
<div class="thumb alt" aria-hidden="true"><div class="labels"><span>Hold</span><span>Sell</span></div></div>
</div>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.