Feedback
Success Check
An SVG outcome mark that draws its ring and then its stroke — a check for success, a cross for failure.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/success-check.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "SuccessCheck" component. No external
dependencies beyond React — the whole thing is one inline SVG.
Contract
- Export a forwardRef div extending React.HTMLAttributes<HTMLDivElement>.
- Props: status?: "success" | "error" (default "success"); size?: number |
"sm" | "md" | "lg" (named sizes map to 32 / 48 / 72 px, default "md");
duration?: number (total draw time in ms, default 900); onComplete?: () =>
void; label?: string (visible caption under the mark); replayKey?: string |
number; aria-label?: string; className merged onto the root via cn().
Behavior
- Two <path> elements share one keyframe: a ring drawn from 12 o'clock, then
the mark. Both carry pathLength={1} and strokeDasharray={1}, so the keyframe
is simply stroke-dashoffset 1 -> 0 regardless of each path's real length —
no circumference math, and it works on <path> in every browser.
- The ring path is two half arcs ("M26 3 A23 23 0 0 1 26 49 A23 23 0 0 1 26 3")
inside a 52x52 viewBox; a single 360-degree arc renders inconsistently.
- The success mark is one polyline; the error mark is ONE path with two
subpaths ("M19 19 L33 33 M33 19 L19 33") so a single dash offset draws
stroke one, then stroke two — the cross is drawn in two beats, not faded in.
- Timing split: the ring gets 55% of `duration`, the mark the remaining 45%
with the ring's duration as its animation-delay. Fill mode "both" holds the
invisible start state through the delay and the finished state afterwards.
- onComplete fires from the mark path's onAnimationEnd — an animation event,
never a setTimeout, so it can't drift or fire after unmount. Keep the
callback in a ref so re-renders don't re-arm it.
- Replay: the <svg> is keyed by `${status}:${replayKey}`. Changing either
remounts the SVG and both strokes redraw from zero. Consumers who prefer it
can drop replayKey and put a key on the component itself instead.
- prefers-reduced-motion (read through useSyncExternalStore with a false
server snapshot, so SSR and hydration agree): no animation at all — both
paths render at stroke-dashoffset 0, i.e. the final frame, and onComplete
fires from an effect on mount. A consumer that navigates away in onComplete
must still work with motion disabled.
Rendering & styling
- Semantic tokens only: the root carries text-[var(--chart-2)] for success and
text-destructive for error, and both paths stroke="currentColor" — so a
single text-* class in className recolors the whole mark (cn() lets the
consumer's token win).
- strokeWidth 3 in a 52-unit viewBox with round caps and joins, so thickness
scales with `size`; the caption is text-foreground and stays readable.
- The <svg> carries role="img" plus an aria-label defaulting to "Success" /
"Error"; the optional caption renders as ordinary text below it.
- Keyframes ship inside the component via a React 19 hoisted <style href
precedence> tag, so dozens of instances still produce one style tag.
Customization levers
- Palette: swap text-[var(--chart-2)] for text-primary or any chart token, or
pass a text-* class in className per instance — both paths follow.
- Pace: `duration` scales the whole choreography; the 55/45 ring-to-mark split
is one constant if you want the ring quicker and the mark more deliberate.
- Weight: strokeWidth is the only line-thickness knob and it is viewBox
relative, so it stays proportional at every size.
- Shape: the mark is a single `d` string per status — swap in an exclamation
bar, a plus, or a custom glyph and the timing, replay and reduced-motion
handling all keep working unchanged.
- Composition: `label` covers the common caption; for a full success screen,
render the component bare and lay out your own heading and CTA around it.Concepts
- Outcome affirmation — a drawn mark reads as the system finished and knows the result, which is why it belongs at the end of a flow rather than anywhere progress is still unknown.
- Normalized path length —
pathLength={1}rescales any path to a length of one, so a singledashoffset: 1 → 0keyframe drives a circle, a check and a cross without measuring any of them. - Two-beat choreography — the ring claims the space, then the mark lands inside it; overlapping the two beats destroys the sense of "settled, then confirmed".
- Subpaths draw in sequence — one path holding two line segments shares a single dash offset, so the cross draws stroke by stroke instead of both arms appearing at once.
- Animation events over timers —
onAnimationEndis the only completion signal that cannot drift from what the user actually saw, and it disappears with the element instead of leaking. - Reduced motion still resolves — with motion disabled the mark renders at its final frame and
onCompletefires immediately, so any redirect or reveal chained to it still happens.