Back to the library
Moiré sync scan
Variant: Phase stagger
Identical geometry and easing, but the loop is split into three offset passes across banded segments of the field, each entering 70ms after the last, and the status resolves only after the final band clears. Use it when the sync covers several sources and the count in the header should tick up rather than flip. The 70ms offset is the widest step that still reads as one gesture at this field width.
loaderstatussyncstaggerop-art
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>Moiré sync scan — phase stagger</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.4s;
--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);
/* Sub-clocks: three offset passes, 70ms apart. */
--_beat: calc(0.3s / var(--speed));
--_lag: calc(0.07s / var(--speed));
--_bar: 3px;
--_gap: 9px;
}
* { 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);
}
.card {
width: min(430px, 92vw);
padding: clamp(16px, 4.5vw, 22px);
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
display: grid;
gap: clamp(12px, 3.4vw, 16px);
}
.head {
display: flex;
align-items: center;
gap: 10px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--clay);
flex: none;
animation: dot-resolve var(--t) var(--ease-in-out) infinite;
/* Resolves only after the last band clears: two stagger steps late. */
animation-delay: calc(var(--_lag) * 2);
}
.label {
font-size: clamp(12px, 3.3vw, 13.5px);
font-weight: 600;
letter-spacing: 0.01em;
}
.meta {
margin-left: auto;
font-size: clamp(11px, 3vw, 12px);
color: var(--clay-d);
font-variant-numeric: tabular-nums;
position: relative;
display: grid;
}
.meta b {
grid-area: 1 / 1;
font-weight: inherit;
opacity: 0;
animation: count var(--t) var(--ease-out) infinite;
}
.meta b:nth-child(1) { animation-delay: calc(var(--_lag) * 0); }
.meta b:nth-child(2) { animation-delay: calc(var(--_lag) * 1); }
.meta b:nth-child(3) { animation-delay: calc(var(--_lag) * 2); }
.field {
position: relative;
height: clamp(84px, 24vw, 104px);
border-radius: calc(9px * var(--round));
overflow: hidden;
background: var(--ivory);
border: 1px solid var(--oat);
}
.layer {
position: absolute;
inset: -20% -40%;
}
.layer-a {
background: repeating-linear-gradient(
90deg,
var(--slate) 0 var(--_bar),
transparent var(--_bar) var(--_gap)
);
opacity: 0.82;
animation: drift-a var(--t) linear infinite;
}
.layer-b {
background: repeating-linear-gradient(
90deg,
var(--clay) 0 var(--_bar),
transparent var(--_bar) var(--_gap)
);
opacity: 0.72;
animation: drift-b var(--t) linear infinite;
}
/* Three horizontal bands, each with its own offset pass. */
.band {
position: absolute;
left: 0;
right: 0;
height: calc(100% / 3);
overflow: hidden;
}
.band:nth-child(3) { top: 0; }
.band:nth-child(4) { top: calc(100% / 3); }
.band:nth-child(5) { top: calc(200% / 3); }
.scan {
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
color-mix(in srgb, var(--paper) 78%, transparent) 45%,
color-mix(in srgb, var(--paper) 92%, transparent) 50%,
transparent 100%
);
animation: scan var(--t) var(--ease-in-out) infinite;
}
.band:nth-child(3) .scan { animation-delay: calc(var(--_lag) * 0); }
.band:nth-child(4) .scan { animation-delay: calc(var(--_lag) * 1); }
.band:nth-child(5) .scan { animation-delay: calc(var(--_lag) * 2); }
.rule {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 2px;
transform-origin: 0 50%;
background: var(--rust);
animation: rule var(--t) var(--ease-out) infinite;
animation-delay: calc(var(--_lag) * 2);
}
.legend {
display: flex;
gap: clamp(8px, 2.6vw, 14px);
font-size: clamp(11px, 2.9vw, 12px);
color: color-mix(in srgb, var(--slate) 58%, var(--ivory));
}
.legend span {
display: inline-flex;
align-items: center;
gap: 6px;
}
.legend i {
width: 14px;
height: 2px;
background: var(--slate);
}
.legend span:nth-child(2) i { background: var(--clay); }
.legend span:nth-child(3) i { background: var(--olive); }
@keyframes drift-a {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(calc(var(--_gap) * 3), 0, 0); }
}
@keyframes drift-b {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(calc(var(--_gap) * -2), 0, 0); }
}
@keyframes scan {
0% { transform: translate3d(-115%, 0, 0); opacity: 0; }
10% { opacity: 1; }
56% { transform: translate3d(115%, 0, 0); opacity: 1; }
58%, 100% { transform: translate3d(115%, 0, 0); opacity: 0; }
}
@keyframes rule {
0% { transform: scaleX(0.02); }
56% { transform: scaleX(1); }
93% { transform: scaleX(1); }
100% { transform: scaleX(0.02); }
}
@keyframes dot-resolve {
0% { background: var(--clay); transform: scale(0.94); }
52% { background: var(--clay); transform: scale(0.94); }
64%, 93% { background: var(--olive); transform: scale(1); }
100% { background: var(--clay); transform: scale(0.94); }
}
@keyframes count {
0%, 14% { opacity: 0; }
26%, 46% { opacity: 1; }
58%, 100% { opacity: 0; }
}
@media (max-width: 380px) {
.card { padding: 14px; gap: 11px; }
.legend { gap: 8px; }
}
@media (prefers-reduced-motion: reduce) {
.layer-a, .layer-b, .scan, .rule { animation: none; }
.scan { opacity: 0; }
.rule { transform: scaleX(1); }
/* Bands keep their stagger, but as opacity only. */
.layer-b {
animation: beat-fade calc(var(--t) * 2) var(--ease-in-out) infinite;
}
.dot {
animation: dot-resolve calc(var(--t) * 2) var(--ease-in-out) infinite;
animation-delay: calc(var(--_lag) * 2);
}
.meta b {
animation-duration: calc(var(--t) * 2);
}
@keyframes beat-fade {
0%, 100% { opacity: 0.34; }
50% { opacity: 0.72; }
}
@keyframes dot-resolve {
0%, 50% { background: var(--clay); }
64%, 93% { background: var(--olive); }
100% { background: var(--clay); }
}
}
</style>
</head>
<body>
<div class="card">
<div class="head">
<span class="dot" aria-hidden="true"></span>
<span class="label">Reindexing library</span>
<span class="meta"><b>1 of 3</b><b>2 of 3</b><b>3 of 3</b></span>
</div>
<div class="field" aria-hidden="true">
<div class="layer layer-a"></div>
<div class="layer layer-b"></div>
<div class="band"><div class="scan"></div></div>
<div class="band"><div class="scan"></div></div>
<div class="band"><div class="scan"></div></div>
<div class="rule"></div>
</div>
<div class="legend">
<span><i></i>Stored</span>
<span><i></i>Scanning</span>
<span><i></i>Synced</span>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.