Confetti
Free · MITHand-written canvas 2D confetti with flipping paper physics, fired over the page from an element or inside a contained canvas via ref, and a no-op under reduced motion.
Installation
$ npx shadcn@latest add https://goodui.dev/r/confetti.jsonThe file lands in components/goodui/confetti.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/confetti works too.
Usage
import { PartyPopper } from "lucide-react"
import { fireConfetti } from "@/components/goodui/confetti"
import { Button } from "@/components/ui/button"
export function PublishBookingPage({ onPublish }: { onPublish: () => Promise<void> }) {
return (
<Button
onClick={async (e) => {
const button = e.currentTarget
await onPublish()
fireConfetti({ element: button, particleCount: 120, spread: 80 })
}}
>
<PartyPopper /> Publish booking page
</Button>
)
}When to use
- A real milestone: first booking received, plan upgraded, onboarding finished, goal reached.
- Payment success screens, fired once from the confirm button or from inside the receipt card.
- Playful moments in consumer apps where a burst from the clicked element feels rewarding.
- Routine saves and confirmations: use a toast-stack success toast; confetti on every action quickly gets annoying.
- The success state itself: pair confetti with success-check so the result is clear even with reduced motion.
- Continuous decorative motion: use particles.
| If you need | Use |
|---|---|
| Burst over the whole page from a button | fireConfetti({ element: e.currentTarget }) |
| Burst at a pointer position | fireConfetti({ origin: { x: e.clientX, y: e.clientY } }) |
| Confetti clipped to one card | <Confetti ref={ref} /> in a relative overflow-hidden parent, then ref.current.fire() |
| Side cannons | two fire calls with angle 60 and 120 from the bottom corners |
| Soft, brand-colored | colors={[…]} shapes={["circle"]} startVelocity 22, gravity 0.6 |
Examples
Contained in a payment card, two cannons
const confetti = React.useRef<ConfettiHandle>(null)
const card = React.useRef<HTMLDivElement>(null)
function onPaid() {
const rect = card.current?.getBoundingClientRect()
if (!rect) return
confetti.current?.fire({ origin: { x: rect.left + 8, y: rect.bottom - 8 }, angle: 60, spread: 50, startVelocity: 30, particleCount: 60 })
confetti.current?.fire({ origin: { x: rect.right - 8, y: rect.bottom - 8 }, angle: 120, spread: 50, startVelocity: 30, particleCount: 60 })
}
<div ref={card} className="relative overflow-hidden rounded-2xl border bg-card p-5">
<Confetti ref={confetti} />
…
</div>Gentle brand-colored burst
fireConfetti({
element: e.currentTarget,
particleCount: 45,
spread: 120,
startVelocity: 22,
gravity: 0.6,
shapes: ["circle"],
colors: ["#a78bfa", "#c4b5fd", "#f0abfc", "#fbcfe8"],
scalar: 0.8,
})API reference
| Prop | Type | Default |
|---|---|---|
| ref Receives { fire(options), clear() } to burst inside this canvas. | React.Ref<ConfettiHandle> | — |
| sound Default `sound` for fire(): "celebrate" (true) or a sound name, when the visitor has sounds on. Off by default. | boolean | SoundName | — |
Also accepts every <canvas> attribute.
Accessibility
- Both canvases are aria-hidden and pointer-events: none, so they never block clicks or reach screen readers.
- Confetti is decoration only: announce the success itself in text (success-check, a toast or a heading).
Motion
- Each piece launches in a cone around angle (default 90°, straight up; spread 70°) at 50–120% of startVelocity (38), slows by decay (0.91) per frame, falls with gravity, wobbles sideways and flips by scaling one axis with cos(), like paper.
- Pieces live about 220 frames (±20%) and fade out over the last 35% of their life; anything that falls 40px below the canvas is dropped.
- Physics is frame-rate independent (normalised to 60 fps, capped at 3 frames per step).
- Reduced motion: fireConfetti() and ref.fire() do nothing at all.
- Canvas 2D drawn by hand, no dependencies. The loop runs only while pieces are alive, pauses while the tab is hidden, and the contained canvas also pauses when scrolled off-screen (IntersectionObserver).
- The page canvas is created on the first fireConfetti() call and removed when the last piece is gone. Device pixel ratio is capped at 2.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- Call fireConfetti() in an event handler or effect, never during render.
- origin is in viewport px (clientX / clientY) for both fireConfetti and ref.fire; the contained canvas converts it.
- angle is in degrees where 90 is up, 0 is right and 180 is left; spread is the cone width in degrees.
- The <Confetti /> canvas is absolute inset-0: put it in a parent with position relative (and overflow-hidden to clip).
- Do not install or import canvas-confetti; the option names (particleCount, spread, startVelocity, decay, ticks, scalar) are already similar.
FAQ
Is the API the same as canvas-confetti?
Close but not identical: the main options share names, while origin is in viewport px instead of 0–1 fractions, and element bursts from an element's center.
Why does nothing happen on my machine?
Your OS probably has reduced motion turned on; confetti is a no-op then. Show the success another way too.
Related
Check or X in a circle that draws its stroke, fills, then pops with a ray-and-ring burst, for payment and booking result screens.
Self-contained toaster with toast() API: cards stack and expand on hover, swipe away with a drag, morph from loading to success or error for promises, and pause their timers on hover.
Canvas particles drift gently, link up with fine lines and move out of the pointer's way; density scales with the area.
Card or overlay that ticks through steps like "Reserving slot…" with spinners, drawn checkmarks and a filling rail, ending on an animated success state.