Back to the library

Job complete, warm

Variant: Warm

The same completion given room: the mark becomes a 54px disc with a single halo rising off it, and three lines of copy stagger in behind over about 700ms. One halo, not three — the restraint is the point, since this is a consumer moment seen a few times a week rather than a milestone. Use it when the job was something the user asked for and waited on.

successcontrolsvgloop

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>Job complete — warm</title>
    <style>
      :root {
        --ivory: #faf9f5;
        --slate: #141413;
        --clay: #d97757;
        --oat: #e3dacc;
        --cycle: 5.6s;
        --out: cubic-bezier(0.16, 0.84, 0.3, 1);
      }

      * {
        box-sizing: border-box;
        margin: 0;
        padding: 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);
      }

      .done {
        /* Grows with the stage instead of sitting at 200px in a 1100px frame.
           Floors at the original width, so the two mobile sizes are unchanged. */
        width: clamp(200px, 34vw, 340px);
        display: grid;
        justify-items: center;
        text-align: center;
        gap: 14px;
        animation: card var(--cycle) linear infinite;
      }

      @keyframes card {
        0% {
          opacity: 0;
        }
        3% {
          opacity: 1;
        }
        95% {
          opacity: 1;
          animation-timing-function: ease;
        }
        100% {
          opacity: 0;
        }
      }

      .mark {
        position: relative;
        /* Same ramp as the card, so the mark holds its 0.27 proportion. */
        width: clamp(54px, 9.2vw, 92px);
        height: clamp(54px, 9.2vw, 92px);
        display: grid;
        place-items: center;
      }

      /* One halo, once. Two would be a performance. */
      .mark .halo {
        position: absolute;
        inset: 0;
        border-radius: 50%;
        border: clamp(1.5px, 0.25vw, 2.5px) solid var(--clay);
        animation: halo var(--cycle) linear infinite;
      }

      @keyframes halo {
        0%,
        9% {
          transform: scale(1);
          opacity: 0;
          animation-timing-function: var(--out);
        }
        10% {
          opacity: 0.45;
        }
        19%,
        100% {
          transform: scale(1.9);
          opacity: 0;
        }
      }

      .mark .disc {
        position: absolute;
        inset: 0;
        border-radius: 50%;
        background: var(--clay);
        animation: disc var(--cycle) linear infinite;
      }

      @keyframes disc {
        0%,
        6% {
          transform: scale(0.92);
          opacity: 0;
          animation-timing-function: var(--out);
        }
        13%,
        100% {
          transform: scale(1);
          opacity: 1;
        }
      }

      .mark svg {
        position: relative;
        width: clamp(26px, 4.4vw, 44px);
        height: clamp(26px, 4.4vw, 44px);
        overflow: visible;
      }

      .mark path {
        fill: none;
        stroke: var(--ivory);
        stroke-width: 3.4;
        stroke-linecap: round;
        stroke-linejoin: round;
        stroke-dasharray: 36;
        animation: draw var(--cycle) linear infinite;
      }

      @keyframes draw {
        0%,
        10.5% {
          stroke-dashoffset: 36;
          animation-timing-function: var(--out);
        }
        17%,
        100% {
          stroke-dashoffset: 0;
        }
      }

      .copy {
        display: grid;
        gap: 5px;
      }

      .title {
        font-size: clamp(15px, 2.45vw, 24px);
        font-weight: 600;
        letter-spacing: -0.015em;
        animation: rise var(--cycle) linear infinite;
      }

      .sub {
        font-size: clamp(12px, 1.85vw, 18px);
        color: color-mix(in srgb, var(--slate) 55%, transparent);
        font-variant-numeric: tabular-nums;
        animation: rise-2 var(--cycle) linear infinite;
      }

      /* A three-beat stagger, ~170ms apart. Written as explicit offsets rather
         than animation-delay so nothing is caught mid-pose on the first cycle. */
      @keyframes rise {
        0%,
        12% {
          opacity: 0;
          transform: translateY(6px);
          animation-timing-function: var(--out);
        }
        19%,
        100% {
          opacity: 1;
          transform: translateY(0);
        }
      }

      @keyframes rise-2 {
        0%,
        15% {
          opacity: 0;
          transform: translateY(6px);
          animation-timing-function: var(--out);
        }
        22%,
        100% {
          opacity: 1;
          transform: translateY(0);
        }
      }

      @keyframes rise-3 {
        0%,
        18% {
          opacity: 0;
          transform: translateY(6px);
          animation-timing-function: var(--out);
        }
        25%,
        100% {
          opacity: 1;
          transform: translateY(0);
        }
      }

      .chip {
        font-size: clamp(11px, 1.8vw, 17px);
        letter-spacing: 0.01em;
        color: color-mix(in srgb, var(--slate) 62%, transparent);
        background: var(--oat);
        border-radius: 999px;
        /* em so the pill keeps its shape as the label scales. */
        padding: 0.36em 0.9em;
        animation: rise-3 var(--cycle) linear infinite;
      }

      @media (prefers-reduced-motion: reduce) {
        .mark .halo {
          animation: halo-rm var(--cycle) linear infinite;
          transform: scale(1.35);
        }
        @keyframes halo-rm {
          0%,
          9% {
            opacity: 0;
          }
          14% {
            opacity: 0.3;
          }
          24%,
          100% {
            opacity: 0;
          }
        }
        .mark .disc {
          animation: fade-rm var(--cycle) linear infinite;
          transform: scale(1);
        }
        .title,
        .sub,
        .chip {
          animation: fade-rm var(--cycle) linear infinite;
          transform: none;
        }
        .sub {
          animation-name: fade-rm-2;
        }
        .chip {
          animation-name: fade-rm-3;
        }
        @keyframes fade-rm {
          0%,
          8% {
            opacity: 0;
          }
          16%,
          100% {
            opacity: 1;
          }
        }
        @keyframes fade-rm-2 {
          0%,
          11% {
            opacity: 0;
          }
          19%,
          100% {
            opacity: 1;
          }
        }
        @keyframes fade-rm-3 {
          0%,
          14% {
            opacity: 0;
          }
          22%,
          100% {
            opacity: 1;
          }
        }
        .mark path {
          animation: fade-rm var(--cycle) linear infinite;
          stroke-dashoffset: 0;
        }
      }
    </style>
  </head>
  <body>
    <div class="done" role="status" aria-label="Your export is ready">
      <div class="mark">
        <span class="halo"></span>
        <span class="disc"></span>
        <svg viewBox="0 0 40 40" aria-hidden="true">
          <path d="M9 20.5 L17 28.5 L31.5 12" />
        </svg>
      </div>
      <div class="copy">
        <div class="title">Your export is ready</div>
        <div class="sub">1,204 rows · 3m 14s</div>
      </div>
      <span class="chip">accounts-2026.csv</span>
    </div>
  </body>
</html>

The variant family

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