One-shot SVG success mark: circle stroke draws, the check follows with slight overlap, and the whole mark settles from 0.9 scale.
npx shadcn@latest add @paragon/success-check"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface SuccessCheckProps extends React.ComponentProps<"svg"> {
/** Rendered size in px. */
size?: number;
strokeWidth?: number;
/** Announced to assistive tech. */
label?: string;
}
/**
* One-shot success mark: the circle stroke draws first, the check follows
* with a slight overlap (~500ms total), while the whole mark settles from
* scale 0.9→1. Success states persist, so it does not loop — remount to
* replay. Under reduced motion it renders fully drawn.
*/
export function SuccessCheck({
size = 48,
strokeWidth = 2.5,
label = "Success",
className,
...props
}: SuccessCheckProps) {
const id = React.useId().replace(/[^a-zA-Z0-9-]/g, "");
// viewBox 52x52, circle r=23 → circumference ≈ 144.5
const circle = 2 * Math.PI * 23;
const check = 30; // path length of the check stroke, slightly padded
return (
<>
<style href={`paragon-success-check-${id}`} precedence="paragon">{`
@keyframes success-circle-${id} {
from { stroke-dashoffset: ${circle}; }
to { stroke-dashoffset: 0; }
}
@keyframes success-check-${id} {
from { stroke-dashoffset: ${check}; }
to { stroke-dashoffset: 0; }
}
@keyframes success-settle-${id} {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
[data-success-check="${id}"] {
animation: success-settle-${id} 250ms var(--ease-out) both;
}
[data-success-check="${id}"] [data-part="circle"] {
stroke-dasharray: ${circle};
animation: success-circle-${id} 300ms var(--ease-out) both;
}
[data-success-check="${id}"] [data-part="check"] {
stroke-dasharray: ${check};
animation: success-check-${id} 260ms 240ms var(--ease-out) both;
}
@media (prefers-reduced-motion: reduce) {
[data-success-check="${id}"],
[data-success-check="${id}"] [data-part] {
animation: none;
}
}
`}</style>
<svg
data-success-check={id}
role="img"
aria-label={label}
viewBox="0 0 52 52"
width={size}
height={size}
fill="none"
className={cn("text-emerald-600 dark:text-emerald-400", className)}
{...props}
>
<circle
data-part="circle"
cx="26"
cy="26"
r="23"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDashoffset={0}
transform="rotate(-90 26 26)"
/>
<path
data-part="check"
d="M16 27l7 7 13-14"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
strokeDashoffset={0}
/>
</svg>
</>
);
}