Verdict thumb, eased
Variant: Eased
A segmented-control thumb that slides from Hold to Sell, stays there briefly, then slides back. Use it on two-option toggles where the user needs to see which way the choice went. The label colour changes inside the thumb itself, so each word turns paper-white only while the thumb covers it. The default ease-in-out speeds up and slows down evenly, which is the right curve for an element moving across the screen. The trip out takes 280ms and the return takes 220ms, and the thumb waits longer on Hold than on Sell, so Hold reads as the side it settles on.
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 — eased</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-in-out: cubic-bezier(0.77, 0, 0.175, 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-in-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-in-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-in-out) infinite; }
.thumb.alt {
display: block;
transform: translateX(100%);
animation: rm-sell calc(var(--t) * 2) var(--ease-in-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.