Back to the library

Liquid sync status

Variant: Standard

An ambient status indicator for a background sync: two counter-rotating lobed rings breathe around a core dot that pulses once per cycle, so a glance tells you the process is still alive. The lobes are drawn with SVG stroke and rotate at linear speed because constant motion should not appear to accelerate, while the core's swell uses ease-in-out so it reads as breathing rather than blinking. Reach for it beside a 'Syncing…' label in a toolbar or settings row. Reduced motion keeps the core's opacity breath at a slower pace and parks the rings.

loaderstatusambientsvgpulse

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>Liquid sync status — standard</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);

  /* File-specific: ring weights and travel for this rung of the axis. */
  --_stroke-a: 3.2;
  --_stroke-b: 2.2;
  --_lobe: 1;
  --_core: 7;
  --_glow: 0.5;
  --_swell: 1.07;
  --_ringspin: calc(var(--t) * 2.5);
}

* { 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(360px, 92vw);
  padding: clamp(18px, 5vw, 26px);
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: clamp(14px, 4vw, 20px);
  background: var(--paper);
  border: 1px solid var(--oat);
  border-radius: calc(16px * var(--round));
}

.dial {
  width: clamp(64px, 18vw, 78px);
  height: clamp(64px, 18vw, 78px);
  display: block;
  overflow: visible;
}

.ring {
  fill: none;
  stroke-linecap: round;
  transform-origin: 50% 50%;
}

.ring-a {
  stroke: var(--clay);
  stroke-width: var(--_stroke-a);
  opacity: 0.9;
  animation: spin-cw var(--_ringspin) linear infinite;
}

.ring-b {
  stroke: var(--olive);
  stroke-width: var(--_stroke-b);
  opacity: 0.75;
  animation: spin-ccw calc(var(--_ringspin) * 1.5) linear infinite;
}

/* The lobed path is scaled, not restroked, so the two rings stay concentric
   while the axis changes how much of the frame they claim. */
.lobes { transform-origin: 50% 50%; transform: scale(var(--_lobe)); }

.core {
  fill: var(--rust);
  transform-origin: 50% 50%;
  animation: breathe var(--t) var(--ease-in-out) infinite;
}

.halo {
  fill: var(--clay);
  transform-origin: 50% 50%;
  opacity: var(--_glow);
  animation: halo var(--t) var(--ease-in-out) infinite;
}

@keyframes spin-cw  { to { transform: rotate(360deg); } }
@keyframes spin-ccw { to { transform: rotate(-360deg); } }

@keyframes breathe {
  0%, 100% { transform: scale(0.94); opacity: 0.72; }
  46%      { transform: scale(var(--_swell)); opacity: 1; }
}

@keyframes halo {
  0%, 100% { transform: scale(0.9); opacity: 0; }
  40%      { opacity: var(--_glow); }
  100%     { transform: scale(1.55); }
}

.copy { display: grid; gap: 4px; min-width: 0; }

.label {
  font-size: clamp(13px, 3.6vw, 15px);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.sub {
  font-size: clamp(11px, 3vw, 12.5px);
  color: color-mix(in oklab, var(--slate) 55%, var(--ivory));
}

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

@media (prefers-reduced-motion: reduce) {
  /* Movement drops; the breath stays so the row still reads as "working". */
  .ring-a, .ring-b, .halo { animation: none; }
  .halo { opacity: 0; }
  .core {
    animation: breathe-soft calc(var(--t) * 2) var(--ease-in-out) infinite;
  }
  @keyframes breathe-soft {
    0%, 100% { opacity: 0.6; transform: scale(1); }
    50%      { opacity: 1;   transform: scale(1); }
  }
}
</style>
</head>
<body>
  <div class="card">
    <svg class="dial" viewBox="0 0 100 100" aria-hidden="true">
      <g class="lobes">
        <!-- Four-lobed closed curve: Art Nouveau arc, not a circle. -->
        <path class="ring ring-a"
              d="M50 8 C72 8 92 28 92 50 C92 72 72 92 50 92 C28 92 8 72 8 50 C8 28 28 8 50 8 Z"
              pathLength="100" stroke-dasharray="17 8" />
        <path class="ring ring-b"
              d="M50 20 C64 26 74 36 80 50 C74 64 64 74 50 80 C36 74 26 64 20 50 C26 36 36 26 50 20 Z"
              pathLength="100" stroke-dasharray="11 6" />
      </g>
      <circle class="halo" cx="50" cy="50" r="calc(var(--_core) * 1px)" />
      <circle class="core" cx="50" cy="50" r="7" />
    </svg>
    <div class="copy">
      <div class="label">Syncing library</div>
      <div class="sub">1,284 items · keeping up</div>
    </div>
  </div>
</body>
</html>

The variant family

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