GodUIGodUI
129Follow on X

Anatomy of Confetti

How a thin canvas-confetti wrapper shares one DEFAULTS object across a button trigger, an imperative ref, and a bare helper, and why origin is normalized to the viewport.

01/04One DEFAULTS object
spread
70° cone: how wide the burst fans
startVelocity
45: how far chips travel from origin
particleCount
120 pieces per fire(); origin.y defaults to 0.7
tsx
const DEFAULTS: ConfettiOptions = {spread: 70,startVelocity: 45,particleCount: 120,origin: { y: 0.7 },disableForReducedMotion: true,};
01

One DEFAULTS object

Confetti is a thin wrapper. It leaves particle physics to canvas-confetti and adds shared defaults, three call sites, and one accessibility flag that the library already understands.

Every fire path merges the same base options. Spread, velocity, count, and a default origin height are tuned once; call sites only override what they need.

origin.y: 0.7 parks bursts in the lower third of the viewport when no explicit origin is passed, so the burst rises from below instead of falling from the top of the screen.

02/04ConfettiButton: rect → viewport fractions
Button rect
e.currentTarget.getBoundingClientRect()
Normalized origin
x = (left + w/2) / innerWidth · y = (top + h/2) / innerHeight
Burst
canvasConfetti({ ...DEFAULTS, origin: { x, y }, ...options })
tsx
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {const rect = e.currentTarget.getBoundingClientRect();const x = (rect.left + rect.width / 2) / window.innerWidth;const y = (rect.top + rect.height / 2) / window.innerHeight;canvasConfetti({ ...DEFAULTS, origin: { x, y }, ...options });onClick?.(e);};
02

ConfettiButton: rect → viewport fractions

The button has no hard-coded origin. On click it measures itself, normalizes its center into 0…1 viewport space, and passes that as origin: { x, y }. The burst comes from the control wherever it sits after layout, scroll, or resize.

options are merged last. You can widen the spread or bump the count and keep the positional origin, unless you override origin too.

03/04Three APIs, one core
ConfettiButton
click → rect origin → fire
Confetti.fire()
imperative ref, toast()-style ergonomics
confetti()
bare helper; all three honor disableForReducedMotion
tsx
React.useImperativeHandle(ref,() => ({  fire: (override?: ConfettiOptions) => {    canvasConfetti({ ...DEFAULTS, ...options, ...override });  },}),[options],);function confetti(options?: ConfettiOptions) {canvasConfetti({ ...DEFAULTS, ...options });}
03

Three APIs, one core

Same merge, three ergonomics:

  1. ConfettiButton: declarative trigger, origin from the click target.
  2. <Confetti ref /> + fire(): imperative, toast-style; returns null (no DOM).
  3. confetti(options?): bare helper for non-React call sites.
04/04Result
04

The result

Defaults set once, an origin read from layout, and three ways to pull the trigger. That is the whole wrapper.

Reduced motion

disableForReducedMotion: true is in DEFAULTS, so every path opts in. canvas-confetti suppresses the burst when the user prefers reduced motion, so GodUI needs no useReducedMotion branch of its own.

Motion Score

ConfettiCC: Paint-triggering
Ccanvas paintcanvas-confetti burst (~2s)
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →