Horizon draft loader
Variant: Hairline
An ambient progress indicator for long generative work: a faint dashed outline is traced ahead of the solid bar that follows it, so the interface always shows the intended shape before the filled state catches up. The trace runs on stroke-dashoffset with a linear clock because it is constant motion, while the fill uses ease-in-out so it reads as a mass moving between two rest points. This hairline sibling claims the least visual weight — 1px strokes, a short travel, low-opacity ghost — for use inline beside body text or in a dense toolbar.
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>Horizon draft loader — 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: 3.6s;
--t: calc(var(--cycle) / var(--speed));
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
/* Weight tokens — the only thing that moves across this family. */
--_stroke: 1px;
--_ghost-op: 0.34;
--_bar-h: 3px;
--_rise: 3px;
--_dot: 4px;
}
* { 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(400px, 90vw);
padding: clamp(16px, 5vw, 26px);
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: center;
gap: 8px;
font-size: clamp(11px, 3vw, 12.5px);
letter-spacing: 0.06em;
text-transform: uppercase;
color: color-mix(in oklch, var(--slate) 58%, var(--ivory));
}
.pip {
width: var(--_dot);
height: var(--_dot);
border-radius: 50%;
background: var(--clay);
animation: pip var(--t) linear infinite;
}
/* The sketch: a dashed outline drawn ahead of the solid form. */
.sketch { display: block; width: 100%; height: auto; overflow: visible; }
.ghost {
fill: none;
stroke: var(--clay);
stroke-width: var(--_stroke);
stroke-linecap: round;
stroke-dasharray: 5 5;
stroke-dashoffset: 0;
opacity: var(--_ghost-op);
/* linear: this is constant motion, not a response to an action. */
animation: trace var(--t) linear infinite;
}
.solid {
fill: none;
stroke: var(--clay-d);
stroke-width: calc(var(--_stroke) * 1.6);
stroke-linecap: round;
stroke-dasharray: 300;
stroke-dashoffset: 300;
animation: settle var(--t) var(--ease-in-out) infinite;
}
.track {
position: relative;
height: var(--_bar-h);
border-radius: 999px;
background: color-mix(in oklch, var(--oat) 72%, var(--paper));
overflow: hidden;
}
.track::after {
content: '';
position: absolute;
inset: 0;
border-radius: 999px;
background: var(--clay);
transform-origin: left center;
transform: scaleX(0.06);
animation: fill var(--t) var(--ease-in-out) infinite;
}
.legend {
display: flex;
justify-content: space-between;
font-size: clamp(10.5px, 2.8vw, 12px);
color: color-mix(in oklch, var(--slate) 50%, var(--ivory));
}
.legend b {
font-weight: 600;
color: var(--clay-d);
display: inline-block;
animation: lift var(--t) var(--ease-in-out) infinite;
}
@keyframes trace {
/* one dash period per cycle keeps the crawl seamless at any --speed */
to { stroke-dashoffset: -60; }
}
@keyframes settle {
0% { stroke-dashoffset: 300; opacity: 0; }
12% { opacity: 1; }
62% { stroke-dashoffset: 0; opacity: 1; }
86% { stroke-dashoffset: 0; opacity: 1; }
100% { stroke-dashoffset: 300; opacity: 0; }
}
@keyframes fill {
0% { transform: scaleX(0.06); }
62% { transform: scaleX(1); }
86% { transform: scaleX(1); }
100% { transform: scaleX(0.06); }
}
@keyframes lift {
0%, 100% { transform: translateY(0); opacity: 0.6; }
62%, 86% { transform: translateY(calc(-1 * var(--_rise))); opacity: 1; }
}
@keyframes pip {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
@media (max-width: 380px) {
.card { padding: 14px; gap: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.ghost {
animation: ghost-breathe calc(var(--t) * 2.2) ease-in-out infinite;
stroke-dashoffset: 0;
}
.solid {
stroke-dashoffset: 0;
animation: solid-breathe calc(var(--t) * 2.2) ease-in-out infinite;
}
.track::after {
transform: scaleX(1);
animation: solid-breathe calc(var(--t) * 2.2) ease-in-out infinite;
}
.legend b { transform: none; animation: solid-breathe calc(var(--t) * 2.2) ease-in-out infinite; }
.pip { animation: solid-breathe calc(var(--t) * 2.2) ease-in-out infinite; }
}
@keyframes ghost-breathe {
0%, 100% { opacity: calc(var(--_ghost-op) * 0.6); }
50% { opacity: var(--_ghost-op); }
}
@keyframes solid-breathe {
0%, 100% { opacity: 0.62; }
50% { opacity: 1; }
}
</style>
</head>
<body>
<div class="card" role="status" aria-label="Drafting">
<div class="head"><span class="pip"></span> Drafting</div>
<svg class="sketch" viewBox="0 0 300 74" preserveAspectRatio="none" aria-hidden="true">
<path class="ghost" d="M6 62 L74 30 L150 46 L222 14 L294 34" />
<path class="solid" d="M6 62 L74 30 L150 46 L222 14 L294 34" />
</svg>
<div class="track"></div>
<div class="legend"><span>outline</span><b>resolved</b></div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.