Back to the library
Sync bar settle
Variant: Ghost
The quietest sibling: the panel border drops away, the track sits at low opacity, and the whole bar is inset so the motion claims less of the frame. Reach for it when the sync is genuinely background work and a full-weight bar would over-report its own importance. Only opacity, stroke width, and the bar's horizontal inset differ from the standard — the keyframe percentages are byte-identical, so the two can sit in the same list without drifting out of phase.
loaderprogressconfirmationminimalsvg
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>Sync bar settle — 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: 3.6s;
--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: the weight axis lives in these tokens only. */
--_check-w: 1.1;
--_track-h: 2px;
--_label-op: 0.38;
--_bar-op: 0.45;
--_inset: 14%;
}
* { box-sizing: border-box; }
body {
margin: 0;
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);
}
.panel {
width: min(380px, 88vw);
padding: clamp(18px, 5vw, 28px);
border-radius: calc(4px * var(--round));
display: grid;
gap: clamp(14px, 4vw, 20px);
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: clamp(11px, 3vw, 12.5px);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.row .label { opacity: var(--_label-op); }
/* The mark slot never resizes, so the bar's collapse cannot shift the row. */
.mark {
width: 18px;
height: 18px;
flex: none;
display: grid;
place-items: center;
opacity: var(--_bar-op);
}
.mark svg { width: 100%; height: 100%; display: block; overflow: visible; }
.mark path {
fill: none;
stroke: var(--olive);
stroke-width: var(--_check-w);
stroke-linecap: square;
stroke-linejoin: miter;
stroke-dasharray: 22;
stroke-dashoffset: 22;
animation: check var(--t) var(--ease-out) infinite;
}
/* Inset narrows the frame the motion claims without touching its timing. */
.track {
position: relative;
margin: 0 var(--_inset);
height: var(--_track-h);
background: var(--oat);
opacity: var(--_bar-op);
transform-origin: 50% 50%;
animation: collapse var(--t) var(--ease-in-out) infinite;
}
.fill {
position: absolute;
inset: 0;
background: var(--clay);
transform-origin: 0% 50%;
transform: scaleX(0);
animation: fill var(--t) linear infinite;
}
/* 0 → 44% fill, hold, 55% → 66% collapse to centre mark, hold result, reset. */
@keyframes fill {
0% { transform: scaleX(0); }
44% { transform: scaleX(1); }
100% { transform: scaleX(1); }
}
@keyframes collapse {
0%, 52% { transform: scaleX(1); opacity: 1; }
68% { transform: scaleX(0.06); opacity: 1; }
92% { transform: scaleX(0.06); opacity: 1; }
99% { transform: scaleX(0.06); opacity: 0; }
100% { transform: scaleX(1); opacity: 0; }
}
@keyframes check {
0%, 66% { stroke-dashoffset: 22; opacity: 0; }
69% { stroke-dashoffset: 22; opacity: 1; }
80% { stroke-dashoffset: 0; opacity: 1; }
94% { stroke-dashoffset: 0; opacity: 1; }
100% { stroke-dashoffset: 0; opacity: 0; }
}
@media (max-width: 380px) {
.panel { padding: 16px; gap: 13px; }
:root { --_inset: 8%; }
}
@media (prefers-reduced-motion: reduce) {
.track { animation: none; transform: none; }
.fill {
transform: scaleX(1);
/* Movement dropped; the state change survives as an opacity cycle at 2x. */
animation: fade-cycle calc(var(--cycle) * 2 / var(--speed)) ease-in-out infinite;
}
.mark path {
stroke-dashoffset: 0;
animation: mark-cycle calc(var(--cycle) * 2 / var(--speed)) ease-in-out infinite;
}
@keyframes fade-cycle {
0%, 44% { opacity: 0.85; }
66%, 94% { opacity: 0.25; }
100% { opacity: 0.85; }
}
@keyframes mark-cycle {
0%, 44% { opacity: 0; }
66%, 94% { opacity: 1; }
100% { opacity: 0; }
}
}
</style>
</head>
<body>
<div class="panel">
<div class="row">
<span class="label">Sync</span>
<span class="mark">
<svg viewBox="0 0 18 18" aria-hidden="true">
<path d="M3 9.6 L7 13.4 L15 4.9" />
</svg>
</span>
</div>
<div class="track"><div class="fill"></div></div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.