GodUIGodUI
91

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.

Confetti is intentionally thin. GodUI doesn't reimplement particle physics — it wraps canvas-confetti with shared defaults, three call sites, and one accessibility flag that the library already understands.

One DEFAULTS object

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.

AnatomyDEFAULTS · spread 70 · n 120
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,
};

origin.y: 0.7 parks bursts in the lower third of the viewport when no explicit origin is passed — celebration from "below," not raining from the top chrome.

ConfettiButton: rect → viewport fractions

The button doesn't fire from a hard-coded point. On click it measures itself, normalizes the center into 0…1 viewport space, and passes that as origin: { x, y } — so the burst reads as coming from the control, wherever it sits after layout, scroll, or resize.

Button originrect → 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);
};

options still win last — you can widen the spread or bump the count without losing the positional origin unless you override it.

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.
Three APIsbutton · ref.fire · confetti()
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 });
}

Reduced motion

disableForReducedMotion: true is in DEFAULTS, so every path opts in. canvas-confetti suppresses the burst when the user prefers reduced motion — no GodUI-side useReducedMotion branch required, and no empty animation to maintain.

The result

Resultthe real component — click to fire

Defaults once, origin from layout, three ways to pull the trigger — that's the whole wrapper.

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub