Success Check
A ring fills in while a checkmark fades, rotates, and draws itself into place.
Installation
npx shadcn@latest add @loomui/success-checkUsage
import { SuccessCheck } from "@/components/ui/success-check"<SuccessCheck active={status === "success"} />How it works
Two elements sit on top of each other. The ring is a persistent SVG
<circle> that fills from transparent to var(--foreground) on a
transition, using the same overshoot curve Switch's thumb settles with.
It's never removed from the DOM, so toggling active back and forth just
reverses that transition normally.
The checkmark is different. It's only mounted while active is true, keyed
by an internal replay counter that increments on every false-to-true edge.
A checkmark that draws itself in should replay in full every time it
appears, and a transition can't do that reliably on an element that never
changes state after it mounts. A fresh element with animations sidesteps
the problem, since animations always run from their from keyframe on
mount no matter what happened before.
The reveal layers four separate animations on the same element (fade,
rotate, blur, a small vertical bob), each with its own easing. Rotate rides
on transform, but the bob rides on the independent translate property
instead of transform: translateY(), so the two don't fight over the same
property and the bob can carry its own bounce curve while the rotate
settles on a plain ease-out. The checkmark draws on a delay (30% of
--dur) via pathLength="1" and an animated stroke-dashoffset, so it
visibly starts after the glyph has already begun materializing rather than
arriving in the same instant.
This is loom's read on a technique from transitions.dev. The original leans on longer, uncapped durations with no scale limit on its properties. Loom's motion contract caps product-surface animation at 300ms, so the durations, travel distances, and blur radius here are all pulled in to fit.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | false | Filled ring + drawn check when true, empty ring when not. |
duration | number | 260 | Reveal duration in ms, exposed as --dur. |
className | string | - | Merged onto the wrapper. |
The wrapper also exposes --size (default 1.5rem) as a CSS custom
property. Override it through style to resize the glyph.
Any other <span> prop is forwarded.
Reduced motion
The ring's transition, the checkmark's four entrance animations, and its stroke-draw animation are all turned off. The component still jumps straight to its filled-and-checked or empty state, it just doesn't animate to get there.