Confetti

Free · MIT

Hand-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.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/confetti.json

The 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

Use it for
  • 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.
Reach for something else
  • 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 needUse
Burst over the whole page from a buttonfireConfetti({ element: e.currentTarget })
Burst at a pointer positionfireConfetti({ 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 cannonstwo fire calls with angle 60 and 120 from the bottom corners
Soft, brand-coloredcolors={[…]} 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

PropTypeDefault
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