Back to the library

Checkmark ink fill

Variant: Ink fill

Colour rises through the badge from the bottom edge and the tick fades up once the ink is past it. The fill is a layer scaled from its bottom origin rather than an animated height, so it stays on the GPU. The slowest of the three checkmarks and the most deliberate — suited to a state that took real work to reach.

successsvgfillconfirmation

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 — ink fill</title>
    <style>
      :root {
        --ivory: #faf9f5;
        --paper: #ffffff;
        --slate: #141413;
        --olive: #788c5d;
        --oat: #e3dacc;
      }

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

      .badge {
        position: relative;
        width: 88px;
        height: 88px;
        display: grid;
        place-items: center;
        border-radius: 50%;
        border: 1.5px solid var(--oat);
        overflow: hidden;
        background: var(--paper);
      }

      /* The fill is a full-size layer scaled from its bottom edge, not a height
         animation — same read, none of the layout cost. 320ms ease-in-out
         because the ink is moving across the badge, not entering it. */
      .badge .ink {
        position: absolute;
        inset: 0;
        background: var(--olive);
        transform-origin: bottom center;
        animation: ink-rise 3.4s infinite;
      }

      .badge svg {
        position: relative;
        width: 40px;
        height: 40px;
        fill: none;
        stroke: var(--paper);
        stroke-width: 3.4;
        stroke-linecap: round;
        stroke-linejoin: round;
        animation: tick-in 3.4s infinite;
      }

      @keyframes ink-rise {
        0% {
          transform: scaleY(0.02);
          animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
        }
        10%,
        88% {
          transform: scaleY(1);
        }
        96%,
        100% {
          transform: scaleY(0.02);
        }
      }

      /* The tick fades up once the ink is past it — before that it would be
         white on white. */
      @keyframes tick-in {
        0%,
        6% {
          opacity: 0;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        13%,
        88% {
          opacity: 1;
        }
        94%,
        100% {
          opacity: 0;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        .badge .ink {
          animation-name: ink-fade;
          transform: scaleY(1);
        }

        @keyframes ink-fade {
          0% {
            opacity: 0;
          }
          12%,
          88% {
            opacity: 1;
          }
          96%,
          100% {
            opacity: 0;
          }
        }
      }
    </style>
  </head>
  <body>
    <div class="badge">
      <span class="ink"></span>
      <svg viewBox="0 0 40 40" role="img" aria-label="Success">
        <path d="M11 20.5 L17.5 27 L29 13" />
      </svg>
    </div>
  </body>
</html>

The variant family

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