Success Checkmark
A check that draws itself inside a soft disc, with a pulsing ring and an optional confetti burst.
Live preview and source code for Success Checkmark
"use client"
import React from "react";
import { motion } from "framer-motion";
export interface SuccessCheckmarkProps {
/** Rendered width of the illustration */
size?: number;
/** Accent colour for the disc, ring and confetti */
accent?: string;
/** Headline under the artwork. Pass `null` for artwork only */
title?: string | null;
/** Supporting line under the title */
description?: string | null;
/** Hides the confetti burst */
confetti?: boolean;
/** Renders the finished state immediately, with no animation */
still?: boolean;
className?: string;
}
const CONFETTI = [
{ angle: -90, distance: 62, color: "#f59e0b", size: 5 },
{ angle: -38, distance: 58, color: "#ec4899", size: 4 },
{ angle: 16, distance: 64, color: "#0ea5e9", size: 5.5 },
{ angle: 62, distance: 54, color: "#22c55e", size: 4 },
{ angle: 128, distance: 60, color: "#8b5cf6", size: 5 },
{ angle: 190, distance: 56, color: "#f43f5e", size: 4.5 },
{ angle: 232, distance: 62, color: "#14b8a6", size: 5 },
];
/**
* SuccessCheckmark — a check that draws itself inside a soft disc, with a
* pulsing ring and an optional confetti burst.
* Renders standalone with zero props.
*/
export default function SuccessCheckmark({
size = 200,
accent = "#16a34a",
title = "All done",
description = "Your changes have been saved.",
confetti = true,
still = false,
className = "",
}: SuccessCheckmarkProps) {
return (
<div
className={className}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 6,
fontFamily: "inherit",
color: "#18181b",
textAlign: "center",
}}
>
<svg
width={size}
height={size}
viewBox="0 0 200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Success"
>
{/* Pulsing halo */}
{!still && (
<motion.circle
cx="100"
cy="100"
r="52"
fill="none"
stroke={accent}
strokeWidth="2"
initial={{ scale: 0.85, opacity: 0.5 }}
animate={{ scale: [0.85, 1.35], opacity: [0.45, 0] }}
transition={{ duration: 2.4, repeat: Infinity, ease: "easeOut" }}
style={{ transformOrigin: "100px 100px" }}
/>
)}
{/* Soft outer disc */}
<motion.circle
cx="100"
cy="100"
r="52"
fill={accent}
fillOpacity="0.12"
initial={still ? false : { scale: 0.4, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", stiffness: 260, damping: 18 }}
style={{ transformOrigin: "100px 100px" }}
/>
{/* Solid inner disc */}
<motion.circle
cx="100"
cy="100"
r="38"
fill={accent}
initial={still ? false : { scale: 0.3, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", stiffness: 300, damping: 17, delay: 0.08 }}
style={{ transformOrigin: "100px 100px" }}
/>
{/* The check, drawn on */}
<motion.path
d="M82 100 L95 113 L119 88"
fill="none"
stroke="#ffffff"
strokeWidth="7"
strokeLinecap="round"
strokeLinejoin="round"
initial={still ? false : { pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.42, ease: "easeOut", delay: still ? 0 : 0.26 }}
/>
{/* Confetti burst */}
{confetti && !still &&
CONFETTI.map((piece, i) => {
const radians = (piece.angle * Math.PI) / 180;
const x = Math.cos(radians) * piece.distance;
const y = Math.sin(radians) * piece.distance;
return (
<motion.circle
key={i}
cx="100"
cy="100"
r={piece.size}
fill={piece.color}
initial={{ x: 0, y: 0, scale: 0, opacity: 0 }}
animate={{ x, y, scale: [0, 1, 0.6], opacity: [0, 1, 0] }}
transition={{ duration: 0.9, ease: "easeOut", delay: 0.34 + i * 0.03 }}
/>
);
})}
</svg>
{title && (
<span style={{ fontSize: 16, fontWeight: 600, letterSpacing: "-0.01em" }}>{title}</span>
)}
{description && (
<span style={{ fontSize: 13.5, color: "#71717a", maxWidth: 280, lineHeight: 1.55 }}>
{description}
</span>
)}
</div>
);
}
A success illustration for confirmation screens: a check that draws itself
inside a soft disc, a halo that keeps pulsing behind it and a confetti burst
that fires once. The disc and the check spring in on mount, so the animation
plays as feedback for something that just finished rather than as a decoration
that was always there. Inline SVG, one accent colour, and a still prop for
the finished frame.
Installation
Copy the source from the Code tab above into your project as
SuccessCheckmark.tsx. The artwork is inline SVG and the copy below it is
styled inline, so there is no image to host and no stylesheet to import.
It imports framer-motion for the spring entrance, the drawn check and the
confetti:
npm install framer-motionUsage
Every prop is optional. size sets both the width and the height in pixels:
import SuccessCheckmark from "./components/SuccessCheckmark";
export default function Example() {
return (
<SuccessCheckmark
size={200}
accent="#16a34a"
title="All done"
description="Your changes have been saved."
/>
);
}confetti={false} keeps the entrance but drops the burst. still renders the
finished state with no animation at all, and title={null} with
description={null} leaves the artwork on its own.
When to reach for it
A confirmation screen after a payment, a signup or a submitted form, where the page's whole job is to say the thing worked.
An empty state that is a good outcome: an inbox with nothing left in it, a queue that is clear, a checklist finished.
A modal or toast at a smaller size with confetti={false}, where the check
should confirm without turning into a celebration.
When not to
If the action can still fail or be undone. The animation reads as final. A pending state, an optimistic update that might roll back, or a step in a longer flow all deserve something quieter.
If the outcome is routine. Confetti for a saved setting or a renamed file
is more noise than feedback. Pass confetti={false} at minimum, or use a small
inline icon instead.
If it needs to replay on demand. The entrance runs once when the component
mounts and there is no prop to trigger it again. Changing the key to remount
is the way to fire it a second time.
Accessibility
The SVG has role="img" and is labelled as a success, so the discs, the check
and every confetti piece are announced as a single image rather than as parts.
The title and description below it are ordinary text.
That label is fixed in the source and does not follow title, so update the
aria-label in the copied file when you change the copy or the language.
The halo pulses on an infinite loop and none of the motion checks
prefers-reduced-motion. Passing still gives you the completed state with no
animation, which is the honest fallback: read the media query yourself and pass
it through.
If the success is the result of something the user just did, the illustration alone will not announce it. Put the message in a live region and let this show the result.
Notes
The confetti pieces have their own fixed seven-colour palette and are not
affected by accent, which only reaches the discs and the halo. Change the
CONFETTI array in the copied file to match a brand palette.
still hides both the halo and the confetti, so it is not a paused frame of
the animation but a cleaner version of the same drawing: disc, inner disc and
check.
The halo is the only part that loops. Everything else settles within about a second of mount and then holds.
Unlike the other illustrations, size is used for both dimensions, so the
artwork is square regardless of how much copy sits under it.