Back to the library

Checkmark draw-on

Variant: Stroke draw

An SVG success mark that draws itself: the ring traces first, the tick follows a beat behind. The sequencing is what sells it — the eye follows the stroke and arrives at the tick, so the confirmation has a beginning and an end rather than just appearing. Best after a submit, where the user is already watching that spot.

successsvgmicro-interactionconfirmation

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>Checkmark — stroke draw</title>
    <style>
      :root {
        --ivory: #faf9f5;
        --slate: #141413;
        --olive: #788c5d;
        --ease-out: cubic-bezier(0.23, 1, 0.32, 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);
      }

      /* Everything runs on one 3.2s loop so the demo repeats: the ring draws in
         480ms, the tick in 260ms behind it, then the mark holds and dissolves.
         In a real success flow, keep only the draw phase and let it play once
         on mount — a confirmation you see twice a day earns 300ms, not a loop. */
      .mark {
        width: 88px;
        height: 88px;
        animation: mark-cycle 3.2s linear infinite;
      }

      .mark circle,
      .mark path {
        fill: none;
        stroke: var(--olive);
        stroke-linecap: round;
        stroke-linejoin: round;
      }

      /* stroke-dashoffset is the one non-transform property here: a draw-on has
         no transform equivalent. It is paint-only — no layout, no reflow. */
      .mark circle {
        stroke-width: 2;
        stroke-dasharray: 176;
        animation: draw-ring 3.2s infinite;
      }

      .mark path {
        stroke-width: 3;
        stroke-dasharray: 44;
        animation: draw-tick 3.2s infinite;
      }

      @keyframes draw-ring {
        0% {
          stroke-dashoffset: 176;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        15%,
        100% {
          stroke-dashoffset: 0;
        }
      }

      @keyframes draw-tick {
        0%,
        6% {
          stroke-dashoffset: 44;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        14%,
        100% {
          stroke-dashoffset: 0;
        }
      }

      /* Hold the finished mark, then fade the group out fast so the restart is
         a dissolve rather than a visible un-drawing. */
      @keyframes mark-cycle {
        0%,
        88% {
          opacity: 1;
        }
        95%,
        100% {
          opacity: 0;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        .mark circle,
        .mark path {
          animation: none;
          stroke-dashoffset: 0;
        }

        .mark {
          animation: mark-cycle-fade 3.2s ease infinite;
        }

        @keyframes mark-cycle-fade {
          0% {
            opacity: 0;
          }
          12%,
          88% {
            opacity: 1;
          }
          96%,
          100% {
            opacity: 0;
          }
        }
      }
    </style>
  </head>
  <body>
    <svg class="mark" viewBox="0 0 60 60" role="img" aria-label="Success">
      <circle cx="30" cy="30" r="28" />
      <path d="M18 31.5 L26.5 40 L42 21" />
    </svg>
  </body>
</html>

The variant family

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