Feedback
Confetti
A DOM-particle celebration burst layer — bump a counter to fire confetti at any point inside a relative container.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/confetti.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "Confetti" component. No animation
library or canvas — DOM <span> particles driven by a delta-time normalized
requestAnimationFrame loop.
Contract
- Export a forwardRef div extending React.HTMLAttributes<HTMLDivElement>.
- Props: fire: number (increment this, e.g. setFire(n => n + 1), to trigger
one burst — 0 and the value present on mount never fire, so a controlled
counter that starts at 0 is safe to wire up directly); particleCount?
(default 80); origin? ({ x: number; y: number }, fraction of the
component's own box, default { x: 0.5, y: 0.5 }); colors? (string[],
default the five chart tokens ["var(--chart-1)" .. "var(--chart-5)"] so a
burst matches any theme without a single hardcoded hex — this is the
differentiator over a typical confetti library, which ships fixed hex
defaults); className.
- Renders one absolute inset-0 pointer-events-none aria-hidden layer with no
visible box of its own. The consumer places it inside a `relative`
container that already holds the content being celebrated.
Behavior
- Compare `fire` against its previous value in an effect; only spawn a burst
when it actually changed and isn't 0 — this makes the trigger a plain
incrementing counter, not a boolean toggle a consumer has to reset.
- On trigger: measure the container's own bounding box, spawn particleCount
particles at origin (as a fraction of that box), each with a random
full-circle direction, a speed magnitude, and a constant upward bias
subtracted from vertical velocity — so the burst reads as "outward and up"
from any origin, including one pinned to the bottom edge. Color cycles
through the colors array in spawn order.
- Physics run in a single rAF loop, integrated with real delta-time each
frame (not a fixed step) so speed is consistent across refresh rates:
velocity decays exponentially (air drag), gravity accumulates linearly,
rotation advances by an angular velocity per particle. Particles fade out
over the last ~20% of their lifetime.
- A particle is removed once its lifetime elapses or it falls well past the
container's bottom edge. The loop stops entirely once no particles remain,
and restarts the next time `fire` changes — it never spins while idle.
- New bursts append to whatever is still animating instead of resetting it,
so mashing the trigger compounds rather than restarting.
- prefers-reduced-motion (useSyncExternalStore over matchMedia, false server
snapshot): spawning is a silent no-op. No fallback animation — a consumer
who wants a visible acknowledgment under reduced motion should pair this
with something like a toast instead.
- Clean up the animation frame on unmount.
Rendering & styling
- No colors of its own beyond what's passed in `colors` — those are plain
CSS color strings (token var(...) references by default) assigned directly
to each particle's inline backgroundColor, so dark mode and rebranding are
just a props change.
- Particles are small absolutely-positioned rounded rectangles; position and
rotation are inline transforms updated every frame, not CSS keyframes,
since each particle's trajectory is unique per burst.
- Root layer is aria-hidden and pointer-events-none so it never intercepts
clicks or gets announced; cn() merges the consumer's className onto it.
Customization levers
- Physics: GRAVITY / DRAG / initial speed and upward-bias ranges are the
only knobs — heavier gravity and higher drag reads as "confetti," lower
drag and speed reads as "floaty sparkle."
- Colors: swap the five chart-token defaults for brand tokens, or pass a
single-color array for a monochrome burst.
- Origin + particleCount: two origins fired on the same `fire` change (one
pinned left, one right) reads as a two-cannon celebration without any new
prop — just render two <Confetti> layers with mirrored origin.x.
- Particle shape: swap the rounded-[1px] rectangle for a rounded-full circle
or a wider aspect ratio for a "ribbon" look.Concepts
- Increment-to-fire trigger —
fireis a monotonic counter, not a boolean; comparing it against its previous value (not truthiness) lets a controlled consumer just dosetFire(n => n + 1)without ever resetting a "should I play" flag. - Delta-time normalized simulation — velocity integrates against real elapsed frame time, not a fixed step, so drag and gravity look identical whether the tab renders at 60fps or 144fps.
- Compounding bursts — a shared particle ref (not per-burst state) means firing again while confetti is still falling adds to it instead of restarting the animation from zero.
- Token-colored particles — colors default to the five
var(--chart-N)tokens instead of hardcoded hex, so the same burst reads correctly in every theme and color scheme with zero prop changes. - Reduced-motion opt-out, no fallback — under
prefers-reduced-motion, spawning is a silent no-op; the component doesn't invent a substitute animation, leaving that choice (e.g. a toast) to the consumer.