Back to Code Bytes
4 min read
Success Check

Description

Success Check runs four animations in parallel when an action completes: the wrapper fades in, rotates from a slight tilt to upright, rises from below with a Y-bob, and de-blurs. At the same time, the SVG path draws its stroke from offset to zero, so the checkmark appears to write itself. The animations share the same duration and ease, so all four properties land together rather than staggering awkwardly. This pattern suits payment confirmations, file uploads, form saves, or any moment where β€œdone” deserves a beat of visual punctuation.

Example

CSS

:root {
  --check-opacity-dur: 550ms;
  --check-rotate-dur: 550ms;
  --check-rotate-from: 80deg;
  --check-bob-dur: 450ms;
  --check-y-amount: 40px;
  --check-blur-dur: 500ms;
  --check-blur-from: 10px;
  --check-path-dur: 550ms;
  --check-path-delay: 80ms;
  --check-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --check-ease-opacity: cubic-bezier(0.22, 1, 0.36, 1);
  --check-ease-rotate: cubic-bezier(0.22, 1, 0.36, 1);
  --check-ease-bob: cubic-bezier(0.34, 1.35, 0.64, 1);
  --check-ease-path: cubic-bezier(0.22, 1, 0.36, 1);
}

/* Wrapper drives the appear animation; it doesn't own any
   sizing or color so you can drop in any icon. */
.t-success-check {
  display: inline-block;
  transform-origin: center;
  opacity: 0;
  will-change: transform, opacity, filter;
}
/* overflow: visible keeps the stroke from clipping while it
   draws; display: block kills the inline whitespace under SVGs. */
.t-success-check svg {
  display: block;
  overflow: visible;
}
/* Stroke-draw setup. Replace 20 with the result of
   path.getTotalLength() for your path; round caps mean any
   sub-pixel overshoot is invisible. */
.t-success-check svg path {
  stroke-dasharray: 20;
  stroke-dashoffset: 20;
}

.t-success-check[data-state="in"] {
  animation:
    t-check-fade var(--check-opacity-dur) var(--check-ease-opacity) forwards,
    t-check-rotate var(--check-rotate-dur) var(--check-ease-rotate) forwards,
    t-check-blur var(--check-blur-dur) var(--check-ease-out) forwards,
    t-check-bob var(--check-bob-dur) var(--check-ease-bob) forwards;
}
.t-success-check[data-state="in"] svg path {
  animation: t-check-draw var(--check-path-dur) var(--check-ease-path)
    var(--check-path-delay, 0ms) forwards;
}

@keyframes t-check-fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes t-check-rotate {
  from {
    transform: rotate(var(--check-rotate-from));
  }
  to {
    transform: rotate(0deg);
  }
}
@keyframes t-check-blur {
  from {
    filter: blur(var(--check-blur-from));
  }
  to {
    filter: blur(0);
  }
}
@keyframes t-check-bob {
  from {
    translate: 0 var(--check-y-amount);
  }
  to {
    translate: 0 0;
  }
}
@keyframes t-check-draw {
  to {
    stroke-dashoffset: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .t-success-check {
    animation: none !important;
    opacity: 1;
  }
  .t-success-check svg path {
    animation: none !important;
    stroke-dashoffset: 0 !important;
  }
}

React

import { useEffect, useRef, useState } from "react";
import "./success-check.css"; // paste the CSS above

function Check() {
  const pathRef = useRef<SVGPathElement>(null);
  const [state, setState] = useState<"out" | "in">("out");
  useEffect(() => {
    const path = pathRef.current;
    if (path) {
      const len = Math.ceil(path.getTotalLength());
      path.style.strokeDasharray = String(len);
      path.style.strokeDashoffset = String(len);
    }
    const id = requestAnimationFrame(() => setState("in"));
    return () => cancelAnimationFrame(id);
  }, []);
  return (
    <span className="t-success-check" data-state={state} aria-hidden="true">
      <svg viewBox="0 0 48 48" fill="none" style={{ width: 56, height: 56 }}>
        <path
          ref={pathRef}
          d="M14 24 l7 7 l13 -15"
          stroke="currentColor"
          strokeWidth="4"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </svg>
    </span>
  );
}

export function SuccessCheck() {
  const [key, setKey] = useState(0);
  return (
    <div>
      <Check key={key} />
      <button onClick={() => setKey((k) => k + 1)}>Replay</button>
    </div>
  );
}

Variables

VariableDefaultNotes
--check-opacity-dur550mssourced from --p10-opacity-dur
--check-rotate-dur550mssourced from --p10-rotate-dur
--check-rotate-from80degsourced from --p10-rotate-from
--check-bob-dur450mssourced from --p10-bob-dur
--check-y-amount40pxsourced from --p10-y-amount
--check-blur-dur500mssourced from --p10-blur-dur
--check-blur-from10pxsourced from --p10-blur-from
--check-path-dur550mssourced from --p10-path-dur
--check-path-delay80mssourced from --p10-path-delay
--check-ease-outcubic-bezier(0.22, 1, 0.36, 1)sourced from --p10-ease-out
--check-ease-opacitycubic-bezier(0.22, 1, 0.36, 1)sourced from --p10-ease-opacity
--check-ease-rotatecubic-bezier(0.22, 1, 0.36, 1)sourced from --p10-ease-rotate
--check-ease-bobcubic-bezier(0.34, 1.35, 0.64, 1)sourced from --p10-ease-bob
--check-ease-pathcubic-bezier(0.22, 1, 0.36, 1)sourced from --p10-ease-path

Credit

Adapted from Success Check on transitions.dev by Jakub Antalik. Original source: github.com/Jakubantalik/transitions.dev.