Back to the library
Handoff baton pass, ghost
Variant: Ghost
A near-transparent reading of the same rail: the token is a hollow outline with a faint fill, the rail and rings sit at low opacity, and the arc is almost flat. Reach for it when the handoff is background information — a footer, a log panel, something the user glances at rather than watches. Because the whole thing is low-contrast, the arriving ring is the only element that reaches full colour, which keeps the current holder legible without raising the overall weight.
handoffprogressstateghoststatus
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 — Ghost</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: 1.5px;
--_rail: 1px;
--_token: 11px;
--_lift-y: -6%;
--_dim: 0.22;
--_veil: 0.45;
}
* { 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 color-mix(in oklab, var(--oat) 60%, var(--paper));
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;
color: color-mix(in oklab, var(--slate) 70%, var(--ivory));
}
.sub {
font-size: clamp(11px, 3vw, 12px);
color: color-mix(in oklab, var(--slate) 40%, 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: color-mix(in oklab, var(--oat) 55%, var(--paper));
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 color-mix(in oklab, var(--oat) 55%, var(--paper));
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) 50%, var(--ivory));
opacity: var(--_dim);
}
/* Hollow token: the fill is a veil, the outline carries the shape. */
.baton {
position: absolute;
top: 6px;
left: 16.666%;
width: var(--_token);
height: var(--_token);
margin-left: calc(var(--_token) / -2);
border-radius: 50%;
border: var(--_ring) solid var(--clay);
background: color-mix(in oklab, var(--clay) 18%, transparent);
opacity: var(--_veil);
animation: hop var(--t) var(--ease-in-out) infinite;
}
@keyframes hop {
0%, 16% { transform: translate(0, 0) scale(1); opacity: var(--_veil); }
22% { transform: translate(0, var(--_lift-y)) scale(0.97); }
38%, 54% { transform: translate(100%, 0) scale(1); opacity: var(--_veil); }
60% { transform: translate(100%, var(--_lift-y)) scale(0.97); }
76%, 92% { transform: translate(200%, 0) scale(1); opacity: var(--_veil); }
/* Fade-return instead of sliding back, so the loop never reads as a reversal. */
96% { transform: translate(200%, 0) scale(1); opacity: var(--_veil); }
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: var(--_veil); }
}
.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.03); }
16% { border-color: var(--clay); transform: scale(1); }
34% { border-color: color-mix(in oklab, var(--oat) 55%, var(--paper)); transform: scale(1); }
100% { border-color: color-mix(in oklab, var(--oat) 55%, var(--paper)); 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.7; }
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: var(--_veil); 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: color-mix(in oklab, var(--oat) 55%, var(--paper)); }
100% { border-color: color-mix(in oklab, var(--oat) 55%, var(--paper)); }
}
@keyframes coreCalm {
0%, 16% { opacity: var(--_veil); }
34% { opacity: 0.08; }
100% { opacity: 0.08; }
}
</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.