Back to the library
Streamlined transit loader
Variant: Slow-in slow-out
Identical geometry and duration, run on a slow-in/slow-out curve so the carrier gathers through the middle of the rail and eases off at both ends. It suits a loader placed near the top of a page where a hard entry would pull the eye off the content. The middle of the pass is roughly twice the speed of the ends, which makes the trailing lines fan out at centre and close back up on arrival.
loaderindeterminateease-in-outhorizontalambient
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>Streamlined transit loader — slow-in slow-out</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: 2.6s;
--t: calc(var(--cycle) / var(--speed));
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--ease-accent: cubic-bezier(0.77, 0, 0.175, 1); /* the run curve under test */
--_rail-h: 2px;
--_trail-step: calc(0.06s / var(--speed));
}
* { 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);
-webkit-font-smoothing: antialiased;
}
.card {
width: min(420px, 92vw);
padding: clamp(18px, 5vw, 28px);
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
display: grid;
gap: clamp(12px, 3.4vw, 18px);
}
.head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.title {
font-size: clamp(12px, 3.4vw, 13.5px);
font-weight: 600;
letter-spacing: 0.01em;
}
.curve {
font-size: clamp(10.5px, 3vw, 11.5px);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--clay-d);
}
.track {
position: relative;
height: clamp(48px, 15vw, 62px);
overflow: hidden;
}
.rail {
position: absolute;
left: 0;
right: 0;
top: calc(50% - var(--_rail-h) / 2);
height: var(--_rail-h);
background: var(--oat);
border-radius: 999px;
}
.runner {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 0;
}
/* The curve gathers mid-rail, so the trail fans at centre and closes at the ends. */
.body, .trail {
position: absolute;
top: 0;
left: 0;
animation: run var(--t) var(--ease-accent) infinite;
}
.body {
width: clamp(30px, 9vw, 38px);
height: clamp(14px, 4.2vw, 17px);
margin-top: calc(clamp(14px, 4.2vw, 17px) / -2);
background: var(--clay);
border-radius: 999px;
clip-path: polygon(0% 24%, 18% 4%, 62% 0%, 100% 50%, 62% 100%, 18% 96%, 0% 76%);
}
.nose {
position: absolute;
right: 14%;
top: 50%;
width: 22%;
height: 3px;
margin-top: -1.5px;
background: var(--paper);
opacity: 0.62;
border-radius: 999px;
}
.trail {
height: 2px;
border-radius: 999px;
background: var(--clay);
}
.trail.a { width: clamp(20px, 6vw, 26px); margin-top: -9px; opacity: 0.42; animation-delay: calc(var(--_trail-step) * -1); }
.trail.b { width: clamp(26px, 8vw, 34px); margin-top: -1px; opacity: 0.30; animation-delay: calc(var(--_trail-step) * -2); }
.trail.c { width: clamp(16px, 5vw, 22px); margin-top: 7px; opacity: 0.42; animation-delay: calc(var(--_trail-step) * -3); }
@keyframes run {
0% { transform: translateX(-14%); opacity: 0; }
10% { opacity: 1; }
82% { opacity: 1; }
100% { transform: translateX(102%); opacity: 0; }
}
.foot {
display: flex;
align-items: center;
gap: 8px;
font-size: clamp(10.5px, 3vw, 11.5px);
color: color-mix(in oklab, var(--slate) 58%, var(--ivory));
}
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--olive);
animation: breathe calc(var(--t) / 2) var(--ease-in-out) infinite;
}
@keyframes breathe {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
@media (max-width: 380px) {
.card { padding: 16px; gap: 12px; }
}
@media (prefers-reduced-motion: reduce) {
.body, .trail {
left: 50%;
transform: translateX(-50%);
animation: hold calc(var(--t) * 2) var(--ease-in-out) infinite;
}
.trail.a { animation-delay: calc(var(--_trail-step) * -2); }
.trail.b { animation-delay: calc(var(--_trail-step) * -4); }
.trail.c { animation-delay: calc(var(--_trail-step) * -6); }
.dot { animation-duration: var(--t); }
@keyframes hold {
0%, 100% { opacity: 0.32; }
50% { opacity: 1; }
}
}
</style>
</head>
<body>
<div class="card">
<div class="head">
<span class="title">Transferring records</span>
<span class="curve">Slow-in slow-out</span>
</div>
<div class="track">
<div class="rail"></div>
<div class="runner">
<div class="trail a"></div>
<div class="trail b"></div>
<div class="trail c"></div>
<div class="body"><span class="nose"></span></div>
</div>
</div>
<div class="foot"><span class="dot"></span> Holding the connection open</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.