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.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add confetti-feedback.enConfetti Feedback is a React confetti component with a ConfettiButton that bursts on click and a useConfetti hook you can fire at any moment. Each piece of confetti is a small DOM element animated with requestAnimationFrame to simulate gravity and flutter, rendered on top of the page through createPortal; Motion is only used to detect the reduced-motion setting.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ConfettiButton/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- Success screens after a payment, a plan upgrade or creating a first project.
- Feedback when an onboarding flow or setup checklist is fully completed.
- Milestones such as a check-in streak, a points total or a sales target reached.
- Celebrating a submitted giveaway entry, survey or event registration.
- Not a good fit: for a small spark at the click position, use Click Spark.
Installation
npx webberui@latest add https://webberui.com/r/confetti-feedback.en.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.
Examples
Firing only after a successful submit
fire can be called at the end of any async flow. Without an origin, the burst starts at the horizontal center, one third down the viewport; the confetti node must be rendered for anything to show.
"use client";
import * as React from "react";
import { useConfetti } from "@/components/ui/confetti-feedback";
export function UpgradeForm() {
const { fire, confetti } = useConfetti();
const [done, setDone] = React.useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const res = await fetch("/api/subscription", { method: "POST" });
if (res.ok) {
setDone(true);
fire();
}
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Upgrade plan</button>
{done && <p role="status">Upgrade complete. The new features are ready to use.</p>}
{confetti}
</form>
);
}Brand colors
ConfettiButton forwards all native button attributes; colors sets the palette each piece picks from at random, and particleCount sets how many pieces fire (up to 80).
import { ConfettiButton } from "@/components/ui/confetti-feedback";
<ConfettiButton
colors={["#0ea5e9", "#6366f1", "#f8fafc"]}
particleCount={70}
className="bg-indigo-600 hover:bg-indigo-500"
onClick={() => markAllDone()}
>
Mark all as done
</ConfettiButton>Bursting from a specific element
origin is in viewport coordinates, so you can compute it from the target element's getBoundingClientRect(). This example fires from the top of a progress bar once it reaches 100%.
"use client";
import * as React from "react";
import { useConfetti } from "@/components/ui/confetti-feedback";
export function GoalProgress({ percent }: { percent: number }) {
const barRef = React.useRef<HTMLDivElement>(null);
const { fire, confetti } = useConfetti();
React.useEffect(() => {
if (percent < 100 || !barRef.current) return;
const r = barRef.current.getBoundingClientRect();
fire({ x: r.left + r.width / 2, y: r.top }, { particleCount: 60 });
}, [percent, fire]);
return (
<>
<div ref={barRef} className="h-2 rounded-full bg-neutral-200">
<div className="h-full rounded-full bg-emerald-500" style={{ width: `${percent}%` }} />
</div>
{confetti}
</>
);
}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
FAQ
Does it work with the Next.js App Router? Any hydration issues?
Yes. The component file already starts with "use client". createPortal needs document, so the confetti layer only renders after mount and only while a burst is active; the server output is empty, so there is no hydration mismatch.
Will it hurt performance?
Each piece is a span whose transform and opacity are updated directly in requestAnimationFrame, so frame-by-frame animation never goes through a React re-render. A burst is capped at 80 pieces with at most 6 bursts kept at once; pieces that fall out of the viewport or exceed their lifetime are recycled, and a finished burst unmounts itself.
Will modals or a fixed header cover the confetti?
The confetti layer is a fixed inset-0 element under document.body with a z-index of 2147483000 and pointer-events-none, so it normally sits above other content without blocking clicks.
What happens with reduced motion?
With "reduce motion" enabled, fire does nothing; ConfettiButton's onClick still runs as usual. Show success feedback separately rather than relying on the confetti alone.
Do I need an extra effects library?
No. The only dependencies are motion, clsx and tailwind-merge, and the confetti physics live in the component source. Without the CLI, copy it from the Manual tab in the installation section.