Components
Confetti Button
A button that bursts confetti from the click position, with physics-based particles that drift and fade.
Preview
Installation
npx shadcn@latest add https://orbit.ruturaj.xyz/r/confetti-buttonCode
"use client";
import { ButtonHTMLAttributes, useEffect, useRef } from "react";
const COLORS = ["#ff6b6b", "#ffd93d", "#6bcb77", "#4d96ff", "#ff922b", "#cc5de8", "#f06595"];
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
color: string;
size: number;
rotation: number;
rotationSpeed: number;
opacity: number;
shape: "rect" | "circle";
}
interface ConfettiButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
particleCount?: number;
}
export function ConfettiButton({
children,
className = "",
disabled,
particleCount = 80,
onClick,
...props
}: ConfettiButtonProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (animRef.current) cancelAnimationFrame(animRef.current);
};
}, []);
function launch(originX: number, originY: number) {
if (animRef.current) cancelAnimationFrame(animRef.current);
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const particles: Particle[] = Array.from({ length: particleCount }, () => ({
x: originX,
y: originY,
vx: (Math.random() - 0.5) * 16,
vy: Math.random() * -14 - 4,
color: COLORS[Math.floor(Math.random() * COLORS.length)],
size: Math.random() * 8 + 4,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.35,
opacity: 1,
shape: Math.random() > 0.5 ? "rect" : "circle",
}));
function tick() {
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
let alive = false;
for (const p of particles) {
if (p.opacity <= 0) continue;
alive = true;
p.x += p.vx;
p.y += p.vy;
p.vy += 0.45;
p.vx *= 0.99;
p.rotation += p.rotationSpeed;
p.opacity -= 0.013;
ctx.save();
ctx.globalAlpha = Math.max(0, p.opacity);
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation);
ctx.fillStyle = p.color;
if (p.shape === "circle") {
ctx.beginPath();
ctx.arc(0, 0, p.size / 2, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2);
}
ctx.restore();
}
if (alive) {
animRef.current = requestAnimationFrame(tick);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
animRef.current = requestAnimationFrame(tick);
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
launch(e.clientX, e.clientY);
onClick?.(e);
}
return (
<>
<canvas
ref={canvasRef}
className="fixed inset-0 pointer-events-none z-50"
aria-hidden="true"
/>
<button
onClick={handleClick}
disabled={disabled}
className={`select-none rounded-[8px] bg-zinc-900 px-6 py-3 text-sm font-medium tracking-wide text-white cursor-pointer hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
{...props}
>
{children}
</button>
</>
);
}