Back to the library

Moiré sync scan

Variant: Slow beat

The same phase-shifting bar grid and single scan pass, stretched to a longer master loop with a wider hold on the resolved state. Reach for this when the sync it reports genuinely takes a while and a fast strip would over-promise. Only the clock changes: the loop nearly doubles and the resolved colour is held roughly twice as long before the next pass, so the strip reads as patient rather than busy.

loaderstatussyncop-artambient

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 — slow beat</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.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);

    /* Sub-clocks: same structure as the single beat, longer beat and no lag. */
    --_beat: calc(0.5s / var(--speed));
    --_lag: calc(0s / 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;
  }

  .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;
    animation: meta-swap var(--t) var(--ease-out) infinite;
  }

  .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;
  }

  .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;
    animation-delay: var(--_lag);
  }

  .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: var(--_lag);
  }

  .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); }
  }

  /* Slow beat: the pass finishes earlier in the loop so the resolved
     state occupies a longer plateau before the reset. */
  @keyframes scan {
    0%   { transform: translate3d(-115%, 0, 0); opacity: 0; }
    8%   { opacity: 1; }
    46%  { transform: translate3d(115%, 0, 0); opacity: 1; }
    48%, 100% { transform: translate3d(115%, 0, 0); opacity: 0; }
  }

  @keyframes rule {
    0%   { transform: scaleX(0.02); }
    46%  { transform: scaleX(1); }
    95%  { transform: scaleX(1); }
    100% { transform: scaleX(0.02); }
  }

  @keyframes dot-resolve {
    0%       { background: var(--clay); transform: scale(0.94); }
    42%      { background: var(--clay); transform: scale(0.94); }
    54%, 95% { background: var(--olive); transform: scale(1); }
    100%     { background: var(--clay); transform: scale(0.94); }
  }

  @keyframes meta-swap {
    0%, 40%  { opacity: 0.45; }
    54%, 95% { opacity: 1; }
    100%     { opacity: 0.45; }
  }

  @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); }
    .layer-b {
      animation: beat-fade calc(var(--t) * 1.6) var(--ease-in-out) infinite;
    }
    .dot {
      animation: dot-resolve calc(var(--t) * 1.6) var(--ease-in-out) infinite;
    }
    .meta {
      animation: meta-swap calc(var(--t) * 1.6) var(--ease-out) infinite;
    }
    @keyframes beat-fade {
      0%, 100% { opacity: 0.34; }
      50%      { opacity: 0.72; }
    }
    @keyframes dot-resolve {
      0%, 45%  { background: var(--clay); }
      58%, 95% { 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">1 of 1</span>
    </div>
    <div class="field" aria-hidden="true">
      <div class="layer layer-a"></div>
      <div class="layer layer-b"></div>
      <div class="scan"></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.