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.
↓Scroll to step through it
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.
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.
Three APIs, one core
Same merge, three ergonomics:
ConfettiButton: declarative trigger, origin from the click target.<Confetti ref />+fire(): imperative, toast-style; returnsnull(no DOM).confetti(options?): bare helper for non-React call sites.
The result
Defaults set once, an origin read from layout, and three ways to pull the trigger. That is the whole wrapper.
- 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
const DEFAULTS: ConfettiOptions = {spread: 70,startVelocity: 45,particleCount: 120,origin: { y: 0.7 },disableForReducedMotion: true,};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.
- 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
const DEFAULTS: ConfettiOptions = {spread: 70,startVelocity: 45,particleCount: 120,origin: { y: 0.7 },disableForReducedMotion: true,};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.
- Button rect
- e.currentTarget.getBoundingClientRect()
- Normalized origin
- x = (left + w/2) / innerWidth · y = (top + h/2) / innerHeight
- Burst
- canvasConfetti({ ...DEFAULTS, origin: { x, y }, ...options })
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);};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.
- ConfettiButton
- click → rect origin → fire
- Confetti.fire()
- imperative ref, toast()-style ergonomics
- confetti()
- bare helper; all three honor disableForReducedMotion
React.useImperativeHandle(ref,() => ({ fire: (override?: ConfettiOptions) => { canvasConfetti({ ...DEFAULTS, ...options, ...override }); },}),[options],);function confetti(options?: ConfettiOptions) {canvasConfetti({ ...DEFAULTS, ...options });}Three APIs, one core
Same merge, three ergonomics:
ConfettiButton: declarative trigger, origin from the click target.<Confetti ref />+fire(): imperative, toast-style; returnsnull(no DOM).confetti(options?): bare helper for non-React call sites.
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
canvas paintcanvas-confetti burst (~2s)