Back to the library

Red wedge publish confirm, single beat

Variant: Single beat

The same wedge-into-circle confirmation compressed into one 160ms beat. The wedge, the fill, the check and the label all start together, and the exit takes 120ms. Use it where publishing is routine, such as a save-and-post flow the user repeats often, so the confirmation stays out of the way. Without stagger the eye takes in one event, and 160ms falls at the short end of the feedback range.

confirmationpublishsnappydiagonalstatus

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 — 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 */
}

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

/* One beat: every part enters over 160ms (14% → 19% of 3.2s) and exits over 120ms (80% → 83.75%). */
@keyframes wedge {
  0%, 14% { opacity: 0; transform: translate(-26px, 26px); animation-timing-function: var(--ease-out); }
  19%, 80% { opacity: 1; transform: translate(0, 0); animation-timing-function: var(--ease-out); }
  83.75%, 100% { opacity: 0; transform: translate(6px, -6px); }
}

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

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

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

@keyframes pending {
  0%, 14% { opacity: 1; transform: translate(0, 0); animation-timing-function: var(--ease-out); }
  17.75%, 83.65% { opacity: 0; transform: translate(6px, -6px); }
  83.75% { opacity: 0; transform: translate(-6px, 6px); animation-timing-function: var(--ease-out); }
  88.75%, 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.