Skip to content

Instantly share code, notes, and snippets.

@mortennajbjerg
Created August 14, 2025 08:51
Show Gist options
  • Save mortennajbjerg/fe10914be38d0e17ad06b66913319544 to your computer and use it in GitHub Desktop.
Save mortennajbjerg/fe10914be38d0e17ad06b66913319544 to your computer and use it in GitHub Desktop.
A confetti hook for React in Typescript
import { useCallback } from "react"
const Colors = [
"#f94144",
"#f3722c",
"#f8961e",
"#f9844a",
"#f9c74f",
"#90be6d",
"#43aa8b",
"#577590",
] as const
type ConfettiOptions = {
count?: number
origin?: { x: number; y: number }
gravity?: number
decay?: number
startVelocity?: number
}
type Particle = {
el: HTMLDivElement
x: number
y: number
vx: number
vy: number
rot: number
vr: number
alpha: number
w: number
h: number
}
const rand = (a: number, b: number): number => Math.random() * (b - a) + a
const DefaultOptions: Required<ConfettiOptions> = {
count: 150,
decay: 0.985,
gravity: 0.35,
origin: { x: 0.5, y: 0.5 },
startVelocity: 14,
}
export const useConfetti = () => {
return useCallback((opts: ConfettiOptions = {}) => {
const options = { ...DefaultOptions, ...opts }
const { count, origin, gravity, decay, startVelocity } = options
const vw = window.innerWidth
const vh = window.innerHeight
const x0 = vw * origin.x
const y0 = vh * origin.y
const layer = document.createElement("div")
layer.style.cssText = `
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 9999;
contain: layout paint size style;
`
document.body.appendChild(layer)
const fragment = document.createDocumentFragment()
const particles: Array<Particle> = []
for (let i = 0; i < count; i++) {
const el = document.createElement("div")
const w = rand(4, 8)
const h = rand(6, 14)
const color = Colors[Math.floor(Math.random() * Colors.length)]
el.style.cssText = `
position: absolute;
left: 0;
top: 0;
width: ${w}px;
height: ${h}px;
background: ${color};
will-change: transform, opacity;
transform-origin: center;
border-radius: ${Math.random() < 0.3 ? "999px" : "2px"};
`
fragment.appendChild(el)
const angle = rand(-Math.PI / 2 - Math.PI / 3, -Math.PI / 2 + Math.PI / 3)
const velocityMultiplier = rand(0.7, 1.3)
particles.push({
alpha: 1,
el,
h,
rot: rand(0, Math.PI * 2),
vr: rand(-0.25, 0.25),
vx: Math.cos(angle) * startVelocity * velocityMultiplier,
vy: Math.sin(angle) * startVelocity * velocityMultiplier,
w,
x: x0,
y: y0,
})
}
layer.appendChild(fragment)
let rafId = 0
const animate = (): void => {
let aliveCount = 0
for (const particle of particles) {
particle.vx *= decay
particle.vy = particle.vy * decay + gravity
particle.x += particle.vx
particle.y += particle.vy
particle.rot += particle.vr
particle.alpha *= decay
if (particle.alpha > 0.05 && particle.y < vh + 24) {
aliveCount++
const { el, x, y, w, h, rot, alpha } = particle
el.style.opacity = alpha.toFixed(3)
el.style.transform = `translate3d(${x - w / 2}px, ${y - h / 2}px, 0) rotate(${rot}rad)`
} else {
particle.el.remove()
}
}
if (aliveCount > 0) {
rafId = requestAnimationFrame(animate)
} else {
cancelAnimationFrame(rafId)
layer.remove()
}
}
animate()
}, [])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment