Back to the library

Grid settle, row stagger

Variant: Row stagger

The same 220ms row beat, offset 55ms per row top to bottom, so the panel resolves as a short cascade instead of a block. Use it when the rows are a reading order the user should follow — the offset implies each value was recomputed in sequence. The readout wipe is delayed until the last row has landed, which is what keeps the cascade from reading as four unrelated twitches.

settlestate-changepanelstaggercascade

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>Grid settle — row 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.2s;
  --t: calc(var(--cycle) / var(--speed));

  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);

  --_step: calc(0.055s / var(--speed)); /* per-row offset */
}

* { box-sizing: border-box; }

html, body { margin: 0; }

body {
  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(400px, 90vw);
  padding: clamp(14px, 4vw, 20px);
  background: var(--paper);
  border: 1px solid var(--oat);
  border-radius: calc(4px * var(--round));
}

.head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding-bottom: clamp(9px, 2.6vw, 12px);
  border-bottom: 1px solid var(--oat);
}

.head b {
  font-size: clamp(11.5px, 3.1vw, 13px);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.head span {
  font-size: clamp(11px, 2.9vw, 11.5px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--clay-d);
}

.rows {
  display: grid;
  gap: clamp(8px, 2.4vw, 11px);
  margin-top: clamp(11px, 3vw, 14px);
}

.row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 10px;
  animation: settle-loop var(--t) linear infinite both;
  animation-delay: calc(var(--_i) * var(--_step));
}

.row:nth-child(1) { --_i: 0; }
.row:nth-child(2) { --_i: 1; }
.row:nth-child(3) { --_i: 2; }
.row:nth-child(4) { --_i: 3; }

.label {
  height: 7px;
  background: var(--oat);
  border-radius: 999px;
}

.row:nth-child(1) .label { width: 62%; }
.row:nth-child(2) .label { width: 44%; }
.row:nth-child(3) .label { width: 71%; }
.row:nth-child(4) .label { width: 52%; }

.dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--oat);
}

.row:nth-child(2) .dot { background: var(--clay); }
.row:nth-child(4) .dot { background: var(--olive); }

.meter {
  margin-top: clamp(13px, 3.4vw, 17px);
  height: 6px;
  background: var(--oat);
  border-radius: 999px;
  overflow: hidden;
}

/* Wipe starts only after the fourth row has landed (3 x 55ms). */
.meter i {
  display: block;
  height: 100%;
  background: var(--clay);
  border-radius: 999px;
  animation: wipe var(--t) linear infinite both;
  animation-delay: calc(3 * var(--_step));
}

.foot {
  margin-top: clamp(9px, 2.6vw, 11px);
  font-size: clamp(11px, 2.9vw, 11.5px);
  letter-spacing: 0.06em;
  color: var(--slate);
  opacity: 0.55;
}

@keyframes settle-loop {
  0%      { transform: translateY(7px); opacity: 0.35; animation-timing-function: var(--ease-out); }
  6.875%  { transform: translateY(0);   opacity: 1; }
  100%    { transform: translateY(0);   opacity: 1; }
}

@keyframes wipe {
  0%      { clip-path: inset(0 100% 0 0); animation-timing-function: var(--ease-out); }
  6.875%  { clip-path: inset(0 32% 0 0); }
  100%    { clip-path: inset(0 32% 0 0); }
}

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

@media (prefers-reduced-motion: reduce) {
  .row {
    /* Offsets kept but scaled with the slower carrier; movement dropped. */
    animation-name: settle-fade;
    animation-duration: calc(var(--cycle) * 2 / var(--speed));
    animation-delay: calc(var(--_i) * var(--_step) * 2);
  }
  .meter i {
    animation-name: hold-fade;
    animation-duration: calc(var(--cycle) * 2 / var(--speed));
    animation-delay: calc(3 * var(--_step) * 2);
    clip-path: inset(0 32% 0 0);
  }
  @keyframes settle-fade {
    0%   { transform: none; opacity: 0.5; }
    12%  { transform: none; opacity: 1; }
    100% { transform: none; opacity: 1; }
  }
  @keyframes hold-fade {
    0%   { opacity: 0.5; }
    12%  { opacity: 1; }
    100% { opacity: 1; }
  }
}
</style>
</head>
<body>
  <div class="panel">
    <div class="head">
      <b>Output settings</b>
      <span>Applied</span>
    </div>
    <div class="rows">
      <div class="row"><div class="label"></div><div class="dot"></div></div>
      <div class="row"><div class="label"></div><div class="dot"></div></div>
      <div class="row"><div class="label"></div><div class="dot"></div></div>
      <div class="row"><div class="label"></div><div class="dot"></div></div>
    </div>
    <div class="meter"><i></i></div>
    <div class="foot">Recalculated from 4 inputs</div>
  </div>
</body>
</html>

The variant family

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