All patterns

Success Check Reveal

A success badge springs into place while the checkmark draws itself along its path.

feedbackminimalpremiumautomatic · finite · starter · ~0.9s
Variant

The animated component in this preview is rendered from the canonical file shown here. The surrounding demo shell only provides context and is not part of the copied code.

135 lines · react + motion only
import { motion, useReducedMotion } from "motion/react";

/**
 * Vibary · Success Check Reveal
 *
 * A success badge that springs into place while the checkmark
 * draws itself along its path.
 *
 * Self-contained: depends only on `motion` (react ships with your app).
 * Works with zero props; tune via `variant`, `size`, `color`.
 * Requires the automatic JSX runtime (default since React 17).
 */

export type SuccessCheckRevealProps = {
  /** Visual character of the motion. */
  variant?: "subtle" | "default" | "playful";
  /** Badge diameter in px. */
  size?: number;
  /** Badge fill color. */
  color?: string;
  /** Accessible label announced to screen readers. */
  label?: string;
  /** Fires once the checkmark has finished drawing. */
  onComplete?: () => void;
};

type VariantConfig = {
  scaleFrom: number;
  rotateFrom: number;
  spring: { type: "spring"; stiffness: number; damping: number };
  checkDelay: number;
  checkDuration: number;
};

const VARIANTS: Record<"subtle" | "default" | "playful", VariantConfig> = {
  // No overshoot: settles straight in. For dense, frequent confirmations.
  subtle: {
    scaleFrom: 0.75,
    rotateFrom: 0,
    spring: { type: "spring", stiffness: 420, damping: 36 },
    checkDelay: 0.06,
    checkDuration: 0.24,
  },
  // Barely-there softness on landing (ratio ~0.95). All-purpose.
  default: {
    scaleFrom: 0.45,
    rotateFrom: 0,
    spring: { type: "spring", stiffness: 340, damping: 35 },
    checkDelay: 0.12,
    checkDuration: 0.3,
  },
  // A single soft rebound and a slight tilt (ratio ~0.82) — lively,
  // never rubbery. Energy comes from the longer travel, not the bounce.
  playful: {
    scaleFrom: 0.3,
    rotateFrom: -6,
    spring: { type: "spring", stiffness: 400, damping: 33 },
    checkDelay: 0.16,
    checkDuration: 0.32,
  },
};

const CHECK_PATH = "M19.5 33.5 L28 42 L44.5 24.5";

export default function SuccessCheckReveal({
  variant = "default",
  size = 64,
  color = "#10B981",
  label = "Success",
  onComplete,
}: SuccessCheckRevealProps) {
  const reduceMotion = useReducedMotion();
  const cfg = VARIANTS[variant];

  // Reduced motion: keep the information, drop the movement.
  // The badge appears fully drawn with a brief opacity fade.
  if (reduceMotion) {
    return (
      <motion.div
        role="img"
        aria-label={label}
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={{ duration: 0.2, ease: "easeOut" }}
        onAnimationComplete={onComplete}
        style={{ width: size, height: size }}
      >
        <svg viewBox="0 0 64 64" width="100%" height="100%" fill="none">
          <circle cx="32" cy="32" r="32" fill={color} />
          <path
            d={CHECK_PATH}
            stroke="#FFFFFF"
            strokeWidth={5}
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
      </motion.div>
    );
  }

  return (
    <motion.div
      role="img"
      aria-label={label}
      initial={{ scale: cfg.scaleFrom, rotate: cfg.rotateFrom, opacity: 0 }}
      animate={{ scale: 1, rotate: 0, opacity: 1 }}
      transition={{
        ...cfg.spring,
        // Opacity fades on its own quick curve; springing it looks muddy.
        opacity: { duration: 0.18, ease: "easeOut" },
      }}
      style={{ width: size, height: size, transformOrigin: "50% 55%" }}
    >
      <svg viewBox="0 0 64 64" width="100%" height="100%" fill="none">
        <circle cx="32" cy="32" r="32" fill={color} />
        <motion.path
          d={CHECK_PATH}
          stroke="#FFFFFF"
          strokeWidth={5}
          strokeLinecap="round"
          strokeLinejoin="round"
          initial={{ pathLength: 0 }}
          animate={{ pathLength: 1 }}
          transition={{
            delay: cfg.checkDelay,
            duration: cfg.checkDuration,
            ease: "easeOut",
          }}
          onAnimationComplete={onComplete}
        />
      </svg>
    </motion.div>
  );
}

About this pattern

Success confirmation for form submits, payments and completed tasks. The circular badge scales in on a soft spring while the checkmark strokes itself in, ending with a quiet settle. It communicates completion clearly without confetti-level drama, which keeps it usable for frequent, low-stakes confirmations as well as one-off payment moments.

Payment confirmedForm submittedTask completedSettings saved

Where it shows up

Screens we drew to show where this motion usually sits. Illustrations, not captures of any product.

  • Ridgeline
    Issues
    Backlog
    Active
    Cycles
    Views
    IssuesNew
    Colourway picker drops a frameRID-412 · PriyaIn progress
    Receipt totals misalign on narrowRID-408 · MarcusTodo
    Session expires without warningRID-401 · DanaIn review
    Export queue stalls past 500 rowsRID-397 · NilsTodo
    Search ranks archived firstRID-390 · PriyaDone
    Issue tracker

    Task completion check with a small, fast settle.

Related patterns