Back to the library

Cutout check toast, single beat

Variant: Single beat

A confirmation toast where the check mark is two paper slabs that slide in from outside the badge and meet at the vertex, as the toast rises into place. Use it for everyday confirmations like saves, sends and payments that should feel settled but not showy. Everything lands together in 220ms on one ease-out, so the check reads as one cut rather than a sequence. The exit takes 160ms, which keeps the dismissal quicker than the arrival.

toastconfirmationcheckentrancefeedback

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 — single beat</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);
}

* { 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 { animation: slab-a var(--t) var(--ease-out) infinite both; }
.slab-b { animation: slab-b var(--t) var(--ease-out) infinite both; }

.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; }

/* 6.875% of 3.2s = 220ms enter; 78–83% = 160ms exit. */
@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); }
}

/* Each slab travels along its own axis so the two meet exactly at the vertex.
   They reset at 85%, after the toast is fully gone. */
@keyframes slab-a {
  0% { transform: translate(-20px, -20px); }
  6.875%, 84% { transform: none; }
  85%, 100% { transform: translate(-20px, -20px); }
}

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

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

@keyframes toast-fade {
  0% { opacity: 0; }
  10%, 78% { opacity: 1; }
  88%, 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: none; }
}
</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.