Back to the library

Console warm-up ready (held state)

Variant: Held state

A fast rise followed by a long dwell at ready: everything resolves in roughly the first fifth of the cycle and then the lit panel simply sits for the remaining four fifths before resetting. This is the tuning for a dashboard tile that lives on screen permanently, where the point is the settled readout and the motion is only an occasional reminder that the feed is live. The reset at the end of the loop is quick and quiet so it never reads as a failure.

statusloaderdialpanelambient

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>Console warm-up ready — held state</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: 6.4s; /* the authored master loop */
  --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);

  --_lamp-1: calc(0.08s / var(--speed));
  --_lamp-2: calc(0.14s / var(--speed));
  --_lamp-3: calc(0.2s / var(--speed));
  --_chrome: #cfd2d4;
}

* { 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);
}

.console {
  width: min(360px, 90vw);
  padding: clamp(14px, 4.4vw, 20px);
  background: var(--paper);
  border: 1px solid var(--oat);
  border-radius: calc(14px * var(--round));
  box-shadow: 0 1px 0 var(--oat) inset;
  display: grid;
  gap: clamp(12px, 3.6vw, 16px);
}

.head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.label {
  font-size: clamp(11px, 3vw, 12px);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--slate);
}

.lamps { display: flex; gap: 6px; }

.lamp {
  width: 20px;
  height: 7px;
  border-radius: 999px;
  background: var(--oat);
  opacity: 0.55;
  transform-origin: left center;
  animation: lamp-latch var(--t) var(--ease-out) infinite;
}
.lamp:nth-child(1) { animation-delay: var(--_lamp-1); }
.lamp:nth-child(2) { animation-delay: var(--_lamp-2); }
.lamp:nth-child(3) { animation-delay: var(--_lamp-3); }

@keyframes lamp-latch {
  0%          { opacity: 0.35; background: var(--oat); transform: scaleX(0.92); }
  4%, 94%     { opacity: 1;    background: var(--clay); transform: scaleX(1); }
  100%        { opacity: 0.35; background: var(--oat); transform: scaleX(0.92); }
}

.face {
  position: relative;
  border-radius: calc(10px * var(--round));
  background: linear-gradient(180deg, var(--ivory), var(--paper));
  border: 1px solid var(--oat);
  padding: clamp(10px, 3vw, 14px);
  overflow: hidden;
}

svg { display: block; width: 100%; height: auto; }

.arc { stroke: var(--oat); }
.arc-live {
  stroke: var(--clay);
  stroke-dasharray: 150;
  stroke-dashoffset: 150;
  animation: arc-fill var(--t) var(--ease-out) infinite;
}

/* Resolve by 18%, dwell to 94%: the readout is the message, not the climb. */
@keyframes arc-fill {
  0%      { stroke-dashoffset: 150; }
  18%     { stroke-dashoffset: 42; }
  94%     { stroke-dashoffset: 42; }
  100%    { stroke-dashoffset: 150; }
}

.needle {
  transform-origin: 110px 78px;
  animation: needle-swing var(--t) var(--ease-in-out) infinite;
}

@keyframes needle-swing {
  0%      { transform: rotate(-58deg); }
  18%     { transform: rotate(34deg); }
  94%     { transform: rotate(34deg); }
  100%    { transform: rotate(-58deg); }
}

.pip { fill: var(--oat); }
.hub { fill: var(--slate); }

.sweep {
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 38%, var(--_chrome) 50%, transparent 62%);
  opacity: 0.5;
  transform: translateX(-120%);
  animation: sweep var(--t) linear infinite;
  pointer-events: none;
}

@keyframes sweep {
  0%, 6%    { transform: translateX(-120%); }
  22%       { transform: translateX(120%); }
  100%      { transform: translateX(120%); }
}

.foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: clamp(10.5px, 2.9vw, 11.5px);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--clay-d);
}

.foot .right { color: var(--olive); }

.bar {
  height: 4px;
  border-radius: 999px;
  background: var(--oat);
  overflow: hidden;
}
.bar i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--rust);
  transform-origin: left center;
  transform: scaleX(0.06);
  animation: bar-fill var(--t) var(--ease-out) infinite;
}

@keyframes bar-fill {
  0%      { transform: scaleX(0.06); }
  18%     { transform: scaleX(1); }
  94%     { transform: scaleX(1); }
  100%    { transform: scaleX(0.06); }
}

@media (max-width: 380px) {
  .console { padding: 13px; gap: 11px; }
  .face { padding: 9px; }
  .lamp { width: 17px; }
}

@media (prefers-reduced-motion: reduce) {
  .needle, .sweep, .bar i, .arc-live { animation: none; }
  .needle { transform: rotate(34deg); }
  .bar i { transform: scaleX(1); }
  .arc-live { stroke-dashoffset: 42; }
  .sweep { display: none; }
  .lamp {
    transform: none;
    animation: lamp-dim calc(var(--t) * 1.5) ease-in-out infinite;
  }
  .lamp:nth-child(1) { animation-delay: var(--_lamp-1); }
  .lamp:nth-child(2) { animation-delay: var(--_lamp-2); }
  .lamp:nth-child(3) { animation-delay: var(--_lamp-3); }
  @keyframes lamp-dim {
    0%, 100% { opacity: 0.6; background: var(--clay); }
    50%      { opacity: 1;   background: var(--clay); }
  }
}
</style>
</head>
<body>
  <div class="console" role="img" aria-label="Device warming up to ready state">
    <div class="head">
      <span class="label">Link status</span>
      <div class="lamps">
        <span class="lamp"></span>
        <span class="lamp"></span>
        <span class="lamp"></span>
      </div>
    </div>

    <div class="face">
      <svg viewBox="0 0 220 96" aria-hidden="true">
        <path class="arc" d="M 42 78 A 68 68 0 0 1 178 78" fill="none" stroke-width="6" stroke-linecap="round" />
        <path class="arc-live" d="M 42 78 A 68 68 0 0 1 178 78" fill="none" stroke-width="6" stroke-linecap="round" />
        <g class="pip">
          <rect x="54" y="44" width="2.5" height="8" rx="1.2" transform="rotate(-52 55 48)" />
          <rect x="108.5" y="22" width="2.5" height="9" rx="1.2" />
          <rect x="163" y="44" width="2.5" height="8" rx="1.2" transform="rotate(52 164 48)" />
        </g>
        <g class="needle">
          <rect x="108.6" y="30" width="2.8" height="50" rx="1.4" fill="currentColor" />
        </g>
        <circle class="hub" cx="110" cy="78" r="5" />
      </svg>
      <span class="sweep"></span>
    </div>

    <div class="bar"><i></i></div>

    <div class="foot">
      <span>Warming</span>
      <span class="right">Ready</span>
    </div>
  </div>
</body>
</html>

The variant family

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