Back to the library

Mirrored sync indicator

Variant: Hairline

The lightest reading of the paired-progress cue — a hairline rule where two thin segments converge on the centre and part again. Reach for it under a table header or at the top edge of a panel, where a sync is worth acknowledging but not worth interrupting for. Travel is cut to a quarter of the track and the seam barely brightens, so at a glance it registers as texture rather than an alert; the timing is identical to its heavier siblings.

loadersyncprogressmirrorsubtle

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>Mirrored sync indicator — 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.2s;
    --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. */
    --_bar-h: 3px;
    --_track-w: min(260px, 70vw);
    --_reach: 25%;
    --_alpha-track: 0.2;
    --_seam-op: 0.32;
    --_seam-w: 1px;
  }

  * { box-sizing: border-box; }

  body {
    margin: 0;
    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(420px, 92vw);
    padding: clamp(18px, 5vw, 28px);
    display: grid;
    justify-items: center;
    gap: clamp(14px, 4vw, 20px);
  }

  .label {
    font-size: clamp(11px, 3vw, 13px);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--slate);
    opacity: 0.45;
  }

  .track {
    position: relative;
    width: var(--_track-w);
    height: var(--_bar-h);
    border-radius: 999px;
    background: color-mix(in oklch, var(--oat) calc(var(--_alpha-track) * 100%), var(--paper));
    overflow: hidden;
  }

  .bar {
    position: absolute;
    top: 0;
    height: 100%;
    width: 50%;
    border-radius: 999px;
  }

  .bar.a {
    left: 0;
    background: var(--clay);
    transform: translateX(calc(-1 * var(--_reach)));
    animation: pushA var(--t) var(--ease-in-out) infinite;
  }

  .bar.b {
    right: 0;
    background: var(--olive);
    transform: translateX(var(--_reach));
    animation: pushB var(--t) var(--ease-in-out) infinite;
  }

  .seam {
    position: absolute;
    left: 50%;
    top: -160%;
    width: var(--_seam-w);
    height: 420%;
    margin-left: calc(var(--_seam-w) / -2);
    background: var(--slate);
    opacity: 0;
    transform: scaleY(0.5);
    animation: seam calc(var(--t) / 2) var(--ease-in-out) infinite;
  }

  @keyframes pushA {
    0%   { transform: translateX(calc(-1 * var(--_reach))); }
    /* contact at the exact half-clock so both sides always arrive together */
    50%  { transform: translateX(0%); }
    100% { transform: translateX(calc(-1 * var(--_reach))); }
  }

  @keyframes pushB {
    0%   { transform: translateX(var(--_reach)); }
    50%  { transform: translateX(0%); }
    100% { transform: translateX(var(--_reach)); }
  }

  @keyframes seam {
    0%, 100% { opacity: 0; transform: scaleY(0.5); }
    46%      { opacity: 0; transform: scaleY(0.5); }
    62%      { opacity: var(--_seam-op); transform: scaleY(1); }
    88%      { opacity: 0; transform: scaleY(0.7); }
  }

  .legend {
    display: flex;
    gap: clamp(14px, 5vw, 24px);
    font-size: clamp(11px, 3vw, 12px);
    color: var(--slate);
    opacity: 0.55;
  }

  .legend span {
    display: inline-flex;
    align-items: center;
    gap: 7px;
  }

  .dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    display: inline-block;
  }

  .dot.a { background: var(--clay); }
  .dot.b { background: var(--olive); }

  .frame {
    padding: clamp(14px, 4vw, 20px);
    border-radius: calc(16px * var(--round));
    background: var(--paper);
    border: 1px solid var(--oat);
    display: grid;
    justify-items: center;
    gap: clamp(14px, 4vw, 18px);
  }

  @media (max-width: 380px) {
    .card { padding: 12px; gap: 12px; }
    .frame { padding: 12px; }
  }

  @media (prefers-reduced-motion: reduce) {
    .bar.a,
    .bar.b {
      animation: none;
      transform: translateX(0%);
    }
    .bar.a { animation: fadeA calc(var(--t) * 2) var(--ease-in-out) infinite; }
    .bar.b { animation: fadeB calc(var(--t) * 2) var(--ease-in-out) infinite; }
    .seam {
      animation: seamQuiet calc(var(--t) * 2) var(--ease-in-out) infinite;
      transform: scaleY(1);
    }
  }

  @keyframes fadeA {
    0%, 100% { opacity: 0.5; }
    50%      { opacity: 1; }
  }

  @keyframes fadeB {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.5; }
  }

  @keyframes seamQuiet {
    0%, 100% { opacity: 0.14; }
    50%      { opacity: var(--_seam-op); }
  }
</style>
</head>
<body>
  <div class="card">
    <div class="frame">
      <div class="label">Syncing both sides</div>
      <div class="track">
        <div class="bar a"></div>
        <div class="bar b"></div>
        <div class="seam"></div>
      </div>
      <div class="legend">
        <span><i class="dot a"></i>Local</span>
        <span><i class="dot b"></i>Remote</span>
      </div>
    </div>
  </div>
</body>
</html>

The variant family

Same idea, one axis moved. Compare them side by side before you commit.