Back to the library

Cutout check toast, cascade

Variant: Cascade

The same toast played as a staggered pass. The panel comes in first, then the short slab, then the long slab, then the title and the receipt line. Each part is offset by 50ms. Use it when the confirmation needs to explain itself: the check reads as if it were drawn, and the text arrives after the mark. Each part keeps the single beat's 220ms enter, so the whole cascade finishes in 420ms and never feels like waiting.

toastconfirmationcheckstaggerentrance

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>Cutout check toast — cascade</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; /* include ONLY if at least one border-radius is wrapped below */
  --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; /* the authored master loop */
  --t: calc(var(--cycle) / var(--speed));

  --ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* include only if used */

  --_shade: color-mix(in oklch, var(--slate) 32%, transparent);
  --_step: 50ms;
}

* { box-sizing: border-box; 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;
  padding: 16px;
}

.toast {
  width: min(340px, 88vw);
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: clamp(12px, 3.5vw, 16px);
  padding: clamp(12px, 3.5vw, 16px) clamp(14px, 4vw, 20px) clamp(12px, 3.5vw, 16px) clamp(12px, 3.5vw, 16px);
  background: var(--slate);
  color: var(--paper);
  border-radius: calc(12px * var(--round));
  box-shadow: 0 12px 28px -14px var(--_shade);
  transform-origin: 50% 100%;
  animation: toast-in var(--t) var(--ease-out) infinite both;
}

.mark {
  width: clamp(40px, 11vw, 48px);
  aspect-ratio: 1;
  background: var(--clay);
  border-radius: calc(8px * var(--round));
  overflow: hidden;
}

.mark svg { display: block; width: 100%; height: 100%; }

.slab {
  fill: none;
  stroke: var(--paper);
  stroke-width: 6;
  stroke-linecap: square;
}

.slab-a { --_i: 1; animation: slab-a var(--t) var(--ease-out) infinite both; }
.slab-b { --_i: 2; animation: slab-b var(--t) var(--ease-out) infinite both; }
.title { --_i: 3; }
.meta { --_i: 4; }

.slab-a, .slab-b, .line {
  animation-delay: calc(var(--_i) * var(--_step) / var(--speed));
}

.title {
  font-size: clamp(14px, 4vw, 16px);
  font-weight: 650;
  letter-spacing: -0.01em;
  line-height: 1.25;
}

.meta {
  margin-top: 2px;
  font-size: clamp(12px, 3.4vw, 13px);
  color: var(--oat);
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
}

.line { animation: line-in var(--t) var(--ease-out) infinite both; animation-delay: calc(var(--_i) * var(--_step) / var(--speed)); }

@keyframes toast-in {
  0% { opacity: 0; transform: translateY(10px) scale(0.96); }
  6.875%, 78% { opacity: 1; transform: none; }
  83%, 100% { opacity: 0; transform: translateY(6px) scale(0.98); }
}

/* Children reset at 86–87% plus up to 200ms of delay (about 93%), which
   stays inside the window where the toast is already invisible. */
@keyframes slab-a {
  0% { transform: translate(-20px, -20px); }
  6.875%, 86% { transform: none; }
  87%, 100% { transform: translate(-20px, -20px); }
}

@keyframes slab-b {
  0% { transform: translate(18px, -20px); }
  6.875%, 86% { transform: none; }
  87%, 100% { transform: translate(18px, -20px); }
}

@keyframes line-in {
  0% { opacity: 0; transform: translateY(4px); }
  6.875%, 86% { opacity: 1; transform: none; }
  87%, 100% { opacity: 0; transform: translateY(4px); }
}

@keyframes toast-fade {
  0% { opacity: 0; }
  10%, 78% { opacity: 1; }
  88%, 100% { opacity: 0; }
}

@keyframes part-fade {
  0% { opacity: 0; }
  10%, 89% { opacity: 1; }
  90%, 100% { opacity: 0; }
}

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

@media (prefers-reduced-motion: reduce) {
  .toast { animation: toast-fade calc(var(--t) * 2) var(--ease-out) infinite both; }
  .slab-a, .slab-b, .line {
    animation: part-fade calc(var(--t) * 2) var(--ease-out) infinite both;
    animation-delay: calc(var(--_i) * var(--_step) * 2 / var(--speed));
  }
}
</style>
</head>
<body>
  <div class="toast" role="status">
    <div class="mark">
      <svg viewBox="0 0 48 48" aria-hidden="true">
        <line class="slab slab-a" x1="13" y1="25" x2="21" y2="33" />
        <line class="slab slab-b" x1="21" y1="33" x2="36" y2="16" />
      </svg>
    </div>
    <div class="copy">
      <p class="line title">Payment sent</p>
      <p class="line meta">Receipt 4821 · $240.00</p>
    </div>
  </div>
</body>
</html>

The variant family

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