Back to the library

Scaffold frame settle (long hold)

Variant: Long hold

The same rails-then-content handoff, retimed for panels that genuinely take a moment: the rails draw over a longer beat and hold noticeably before the blocks arrive, so the frame is legible as its own state. Reach for it when the fetch behind the placeholder is slow enough that an instant fill would look like a lie. Geometry and easing are identical to the single-beat sibling; only the master cycle and the block cue lengthen.

skeletonloadingplaceholderstate-changestructure

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>Scaffold frame settle — long hold</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: 5.2s;
  --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);

  /* File-specific: the frame is allowed to stand alone for a full beat. */
  --_block-delay: calc(1.55s / var(--speed));
}

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

.stage {
  width: min(360px, 88vw);
  padding: clamp(10px, 3vw, 16px);
}

.card {
  position: relative;
  background: var(--paper);
  border: 1px solid var(--oat);
  border-radius: calc(14px * var(--round));
  padding: clamp(16px, 5vw, 22px);
  display: grid;
  gap: clamp(9px, 2.6vw, 13px);
}

.rail {
  position: absolute;
  background: var(--clay);
  opacity: 0;
}

.rail-v {
  top: clamp(10px, 3vw, 14px);
  bottom: clamp(10px, 3vw, 14px);
  width: 1px;
  transform-origin: 50% 0%;
  animation: rail-v var(--t) var(--ease-out) infinite;
}

.rail-h {
  left: clamp(10px, 3vw, 14px);
  right: clamp(10px, 3vw, 14px);
  height: 1px;
  transform-origin: 0% 50%;
  animation: rail-h var(--t) var(--ease-out) infinite;
}

.rail-v.a { left: clamp(10px, 3vw, 14px); }
.rail-v.b { right: clamp(10px, 3vw, 14px); }
.rail-h.c { top: clamp(10px, 3vw, 14px); }
.rail-h.d { bottom: clamp(10px, 3vw, 14px); }

/* Percentages retimed, not rewritten: draw ends at 11% of a 5.2s loop
   (~570ms) and the frame holds alone until the blocks are cued. */
@keyframes rail-v {
  0%   { opacity: 0; transform: scaleY(0.04); }
  11%  { opacity: 0.85; transform: scaleY(1); }
  66%  { opacity: 0.85; transform: scaleY(1); }
  82%  { opacity: 0; transform: scaleY(1); }
  100% { opacity: 0; transform: scaleY(1); }
}

@keyframes rail-h {
  0%   { opacity: 0; transform: scaleX(0.04); }
  11%  { opacity: 0.85; transform: scaleX(1); }
  66%  { opacity: 0.85; transform: scaleX(1); }
  82%  { opacity: 0; transform: scaleX(1); }
  100% { opacity: 0; transform: scaleX(1); }
}

.block {
  height: clamp(10px, 3vw, 13px);
  border-radius: calc(4px * var(--round));
  background: var(--slate);
  opacity: 0;
  transform: translateY(6%) scale(0.97);
  transform-origin: 0% 50%;
  animation: block-in var(--t) var(--ease-out) infinite;
  animation-delay: var(--_block-delay);
}

.block.w1 { width: 62%; }
.block.w2 { width: 100%; }
.block.w3 { width: 44%; background: var(--clay-d); }

.block.w2, .block.w3 { height: clamp(8px, 2.4vw, 10px); }

@keyframes block-in {
  0%   { opacity: 0; transform: translateY(6%) scale(0.97); }
  5.6% { opacity: 1; transform: translateY(0) scale(1); }
  36%  { opacity: 1; transform: translateY(0) scale(1); }
  50%  { opacity: 0; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(0) scale(1); }
}

.caption {
  margin: clamp(10px, 3vw, 14px) 2px 0;
  font-size: clamp(11px, 2.9vw, 12px);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--slate);
  opacity: 0.42;
}

@media (max-width: 380px) {
  .card { padding: 15px; }
  .stage { padding: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  /* 1.5x here rather than 2x: the authored loop is already slow. */
  .rail-v, .rail-h {
    animation: rail-fade calc(var(--cycle) * 1.5 / var(--speed)) var(--ease-in-out) infinite;
    transform: none;
  }
  .block {
    transform: none;
    animation: block-fade calc(var(--cycle) * 1.5 / var(--speed)) var(--ease-in-out) infinite;
    animation-delay: calc(1.55s / var(--speed));
  }
  @keyframes rail-fade {
    0%, 100% { opacity: 0.2; }
    30%, 60% { opacity: 0.7; }
  }
  @keyframes block-fade {
    0%, 100% { opacity: 0.35; }
    18%, 40% { opacity: 1; }
  }
}
</style>
</head>
<body>
  <div class="stage">
    <div class="card">
      <span class="rail rail-v a"></span>
      <span class="rail rail-v b"></span>
      <span class="rail rail-h c"></span>
      <span class="rail rail-h d"></span>
      <span class="block w1"></span>
      <span class="block w2"></span>
      <span class="block w3"></span>
    </div>
    <p class="caption">Loading panel</p>
  </div>
</body>
</html>

The variant family

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