Back to the library

Red wedge publish confirm, slow beat

Variant: Slow beat

The same wedge-into-circle confirmation played as one slower beat. Everything enters together over 280ms, exits over 180ms, and the result holds on screen for longer in a 4.4s loop. Use it for weightier or less frequent actions, like sending to a large audience, where the confirmation should feel deliberate. The entrance stays under 300ms and the exit stays shorter than the entrance, so it feels unhurried without slowing the interface.

confirmationpublishdeliberatediagonalstatus

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>Red wedge publish confirm — slow 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: 4.4s; /* the authored master loop */
  --t: calc(var(--cycle) / var(--speed));

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

* { box-sizing: border-box; }

body {
  margin: 0;
  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);
}

.card {
  width: min(360px, 88vw);
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: clamp(12px, 4vw, 20px);
  padding: clamp(12px, 4vw, 18px);
  background: var(--paper);
  border: 2px solid var(--slate);
  border-radius: calc(4px * var(--round));
  box-shadow: 5px 5px 0 var(--slate);
}

.mark {
  display: block;
  width: clamp(68px, 22vw, 100px);
  height: clamp(68px, 22vw, 100px);
  background: var(--oat);
  border-radius: calc(2px * var(--round));
  overflow: hidden;
}

.rail { stroke: var(--slate); stroke-width: 1.5; stroke-dasharray: 2 3; }
.ring { fill: none; stroke: var(--slate); stroke-width: 3; }

.fill {
  fill: var(--slate);
  transform-box: fill-box;
  transform-origin: center;
  animation: fill var(--t) linear infinite both;
}

.check {
  fill: none;
  stroke: var(--paper);
  stroke-width: 5;
  stroke-linecap: butt;
  stroke-linejoin: miter;
  stroke-dasharray: 1;
  animation: check var(--t) linear infinite both;
}

.wedge {
  fill: var(--rust);
  animation: wedge var(--t) linear infinite both;
}

.copy { display: grid; gap: 4px; min-width: 0; }

.kicker {
  font-size: clamp(11px, 3vw, 12px);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--rust);
}

.status { display: grid; }
.status > span {
  grid-area: 1 / 1;
  font-size: clamp(18px, 6vw, 26px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
}
.pending { animation: pending var(--t) linear infinite both; }
.done { animation: done var(--t) linear infinite both; }

.sub {
  font-size: clamp(12px, 3.4vw, 14px);
  color: color-mix(in oklch, var(--slate) 62%, var(--paper));
}

/* Slow beat on a 4.4s loop: enter 280ms (14% → 20.36%), exit 180ms (80% → 84.09%).
   The longer loop buys a longer hold on the result, not a slower entrance. */
@keyframes wedge {
  0%, 14% { opacity: 0; transform: translate(-26px, 26px); animation-timing-function: var(--ease-out); }
  20.36%, 80% { opacity: 1; transform: translate(0, 0); animation-timing-function: var(--ease-out); }
  84.09%, 100% { opacity: 0; transform: translate(6px, -6px); }
}

@keyframes fill {
  0%, 14% { opacity: 0; transform: scale(0.94); animation-timing-function: var(--ease-out); }
  20.36%, 80% { opacity: 1; transform: scale(1); animation-timing-function: var(--ease-out); }
  84.09%, 100% { opacity: 0; transform: scale(1); }
}

@keyframes check {
  0%, 14% { opacity: 0; stroke-dashoffset: 1; animation-timing-function: var(--ease-out); }
  20.36%, 80% { opacity: 1; stroke-dashoffset: 0; animation-timing-function: var(--ease-out); }
  84.09% { opacity: 0; stroke-dashoffset: 0; }
  84.2%, 100% { opacity: 0; stroke-dashoffset: 1; }
}

@keyframes done {
  0%, 14% { opacity: 0; transform: translate(-6px, 6px); animation-timing-function: var(--ease-out); }
  20.36%, 80% { opacity: 1; transform: translate(0, 0); animation-timing-function: var(--ease-out); }
  84.09%, 100% { opacity: 0; transform: translate(0, 0); }
}

@keyframes pending {
  0%, 14% { opacity: 1; transform: translate(0, 0); animation-timing-function: var(--ease-out); }
  18.09%, 84% { opacity: 0; transform: translate(6px, -6px); }
  84.09% { opacity: 0; transform: translate(-6px, 6px); animation-timing-function: var(--ease-out); }
  90.45%, 100% { opacity: 1; transform: translate(0, 0); }
}

@media (max-width: 380px) {
  .card { gap: 12px; padding: 12px; box-shadow: 4px 4px 0 var(--slate); }
}

@media (prefers-reduced-motion: reduce) {
  .wedge, .fill, .check, .pending, .done {
    transform: none !important;
    animation-duration: calc(var(--t) * 2);
  }
  .check { stroke-dashoffset: 0 !important; }
}
</style>
</head>
<body>
  <div class="card">
    <svg class="mark" viewBox="0 0 100 100" aria-hidden="true">
      <line class="rail" x1="0" y1="102" x2="62" y2="40" />
      <circle class="ring" cx="62" cy="40" r="24" />
      <circle class="fill" cx="62" cy="40" r="24" />
      <path class="check" d="M51 40 L58.5 47.5 L73 33" pathLength="1" />
      <polygon class="wedge" points="49,53 31.3,83.4 18.6,70.7" />
    </svg>
    <div class="copy" role="status">
      <span class="kicker">Bulletin 14</span>
      <span class="status">
        <span class="pending">Sending…</span>
        <span class="done">Published</span>
      </span>
      <span class="sub">To 1,204 readers</span>
    </div>
  </div>
</body>
</html>

The variant family

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