Back to the library
Handoff baton pass, hairline
Variant: Hairline
The lightest reading of the handoff rail: 1px rings and rail, a smaller token, and a shallower arc so the motion barely disturbs the layout around it. Use it inside dense tables or sidebars where a status rail sits next to real content and must not out-shout it. Timing and easing are identical to Standard — only stroke, token size, arc height, and the dim level move.
handoffprogressstatehairlinestatus
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>Handoff baton pass — Hairline</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: 4.2s;
--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);
/* File-specific: rail geometry and per-variant weight. */
--_ring: 1px;
--_rail: 1px;
--_token: 8px;
--_lift-y: -9%;
--_dim: 0.28;
}
* { box-sizing: border-box; }
html, body { 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(420px, 92vw);
padding: clamp(18px, 5vw, 26px) clamp(16px, 5vw, 26px);
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
}
.head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: clamp(20px, 6vw, 28px);
}
.title {
font-size: clamp(12.5px, 3.4vw, 14px);
font-weight: 600;
letter-spacing: 0.01em;
}
.sub {
font-size: clamp(11px, 3vw, 12px);
color: color-mix(in oklab, var(--slate) 52%, var(--ivory));
}
.rail {
position: relative;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: start;
}
.line {
position: absolute;
left: 16.666%;
right: 16.666%;
top: calc(var(--_token) + 6px);
height: var(--_rail);
background: var(--oat);
border-radius: 999px;
}
.station {
position: relative;
display: grid;
justify-items: center;
gap: 10px;
}
.dot {
position: relative;
width: calc(var(--_token) + 12px);
height: calc(var(--_token) + 12px);
border-radius: 50%;
border: var(--_ring) solid var(--oat);
background: var(--paper);
display: grid;
place-items: center;
}
.core {
width: var(--_token);
height: var(--_token);
border-radius: 50%;
background: var(--clay);
opacity: 0;
}
.label {
font-size: clamp(10.5px, 2.9vw, 11.5px);
text-align: center;
color: color-mix(in oklab, var(--slate) 55%, var(--ivory));
opacity: var(--_dim);
}
.baton {
position: absolute;
top: calc(6px + (var(--_ring) * 0));
left: 16.666%;
width: var(--_token);
height: var(--_token);
margin-left: calc(var(--_token) / -2);
margin-top: 6px;
border-radius: 50%;
background: var(--clay);
box-shadow: 0 0 0 calc(var(--_ring) + 1px) color-mix(in oklab, var(--clay) 14%, transparent);
animation: hop var(--t) var(--ease-in-out) infinite;
}
@keyframes hop {
0%, 16% { transform: translate(0, 0) scale(1); }
22% { transform: translate(0, var(--_lift-y)) scale(0.97); }
38%, 54% { transform: translate(100%, 0) scale(1); }
60% { transform: translate(100%, var(--_lift-y)) scale(0.97); }
76%, 92% { transform: translate(200%, 0) scale(1); }
/* Fade-return instead of sliding back, so the loop never reads as a reversal. */
96% { transform: translate(200%, 0) scale(1); opacity: 1; }
97.5% { transform: translate(200%, 0) scale(0.94); opacity: 0; }
98% { transform: translate(0, 0) scale(0.94); opacity: 0; }
100% { transform: translate(0, 0) scale(1); opacity: 1; }
}
.s1 .dot { animation: held var(--t) var(--ease-out) infinite; }
.s2 .dot { animation: held var(--t) var(--ease-out) calc(0.38 * var(--t)) infinite; }
.s3 .dot { animation: held var(--t) var(--ease-out) calc(0.76 * var(--t)) infinite; }
@keyframes held {
0% { border-color: var(--clay); transform: scale(1); }
6% { border-color: var(--clay); transform: scale(1.04); }
16% { border-color: var(--clay); transform: scale(1); }
34% { border-color: var(--oat); transform: scale(1); }
100% { border-color: var(--oat); transform: scale(1); }
}
.s1 .label { animation: lit var(--t) var(--ease-out) infinite; }
.s2 .label { animation: lit var(--t) var(--ease-out) calc(0.38 * var(--t)) infinite; }
.s3 .label { animation: lit var(--t) var(--ease-out) calc(0.76 * var(--t)) infinite; }
@keyframes lit {
0%, 20% { opacity: 0.85; }
36% { opacity: var(--_dim); }
100% { opacity: var(--_dim); }
}
@media (max-width: 380px) {
.card { padding: 16px 12px; }
.head { margin-bottom: 18px; }
}
@media (prefers-reduced-motion: reduce) {
.baton { display: none; }
.s1 .dot, .s2 .dot, .s3 .dot { animation: heldCalm calc(var(--t) * 2) var(--ease-out) infinite; }
.s2 .dot { animation-delay: calc(0.38 * var(--t) * 2); }
.s3 .dot { animation-delay: calc(0.76 * var(--t) * 2); }
.s1 .label, .s2 .label, .s3 .label { animation: lit calc(var(--t) * 2) var(--ease-out) infinite; }
.s2 .label { animation-delay: calc(0.38 * var(--t) * 2); }
.s3 .label { animation-delay: calc(0.76 * var(--t) * 2); }
.core { opacity: 1; animation: coreCalm calc(var(--t) * 2) var(--ease-out) infinite; }
.s2 .core { animation-delay: calc(0.38 * var(--t) * 2); }
.s3 .core { animation-delay: calc(0.76 * var(--t) * 2); }
}
@keyframes heldCalm {
0%, 16% { border-color: var(--clay); }
34% { border-color: var(--oat); }
100% { border-color: var(--oat); }
}
@keyframes coreCalm {
0%, 16% { opacity: 1; }
34% { opacity: 0.12; }
100% { opacity: 0.12; }
}
</style>
</head>
<body>
<div class="card">
<div class="head">
<span class="title">Request handoff</span>
<span class="sub">in transit</span>
</div>
<div class="rail">
<div class="line"></div>
<div class="station s1"><div class="dot"><div class="core"></div></div><div class="label">Intake</div></div>
<div class="station s2"><div class="dot"><div class="core"></div></div><div class="label">Review</div></div>
<div class="station s3"><div class="dot"><div class="core"></div></div><div class="label">Release</div></div>
<div class="baton"></div>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.