Confetti Feedback
A hand-built particle confetti celebration — bursting from an origin point, each piece with its own initial velocity, gravity, rotation, and drift, updated on rAF and reclaimed once it lands.
npx shadcn@latest add https://webberui.com/r/confetti-feedback.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ConfettiFeedback />
Installation
npx shadcn@latest add https://webberui.com/r/confetti-feedback.jsonOr, once registries are configured in components.json, install it as @webberui/confetti-feedback.
Usage
The simplest way in is ConfettiButton, which bursts confetti from the center of the button on click and runs onClick as usual:
import { ConfettiButton } from "@/components/ui/confetti-feedback";
<ConfettiButton onClick={() => save()}>Celebrate 🎉</ConfettiButton>When you need to burst at an arbitrary moment from arbitrary coordinates, use the useConfetti hook instead. It returns fire plus a confetti node that must be rendered into the page:
import { useConfetti } from "@/components/ui/confetti-feedback";
function CheckoutDone() {
const { fire, confetti } = useConfetti();
return (
<>
<button
onClick={(e) => {
const r = e.currentTarget.getBoundingClientRect();
fire({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
}}
>
Done
</button>
{confetti}
</>
);
}In fire(origin?, options?), origin is in viewport coordinates (defaulting to the top center of the screen when omitted), and options can carry particleCount and colors.
Props
ConfettiButton
Forwards all native button attributes, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
particleCount | number | 50 | Number of particles per burst, clamped between 0 and 80 |
colors | string[] | seven built-in colors | Candidate confetti colors, picked at random per piece |
onClick | (e) => void | — | Runs as usual after the burst |
className | string | — | Appended to the button styling |
useConfetti()
| Returns | Type | Description |
|---|---|---|
fire | (origin?, options?) => void | Triggers one burst; origin is in viewport coordinates { x, y } |
confetti | React.ReactNode | The node you must render; internally it is stacked on top via createPortal |
options supports particleCount (capped at 80) and colors.
How it works and accessibility
- Particles are small DOM color blocks whose
transform(offset, rotation, drifting sway) is updated frame by frame on rAF, with no React re-render - Each piece of confetti carries a random initial velocity and gravity, and is reclaimed once it falls past the bottom of the viewport or exceeds its maximum lifetime; once a whole burst is cleared it unmounts automatically
dtis capped, so particles do not teleport when the tab is switched back or frames are dropped- A single burst is capped at 80 particles, and rapid clicking keeps only the most recent few bursts — older bursts unmount and their rAF is stopped
cancelAnimationFramecleans up on unmount, so nothing updates after unmounting- The layer is decorative only, carrying
aria-hiddenandpointer-events-noneso it does not interfere with the pointer or with assistive technology - When the user has "reduce motion" enabled at the system level, nothing bursts at all, but the button's
onClickaction still runs