Particle burst
Also called click particles, emoji burst, particle explosion, or Cool Mode
Small particles, such as dots, emoji, or icons, that shoot out from the pointer on each click or while a button is held, then fall and fade.
import { useRef } from 'react';
import { useReducedMotion } from 'motion/react';
type Particle = Record<'x' | 'y' | 'vx' | 'vy' | 'age' | 'life', number> & {
glyph: string;
};
type CheerButtonProps = { count: number; onCheer: () => void };
const GLYPHS = ['🎉', '✨', '🚀', '💖'];
const GRAVITY = 1100; // px/s²
export function CheerButton({ count, onCheer }: CheerButtonProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const live = useRef<Particle[]>([]);
const frame = useRef(0);
const reduceMotion = useReducedMotion();
// Runs only while particles are alive, then stops by itself.
function step(context: CanvasRenderingContext2D, last: number) {
frame.current = requestAnimationFrame((now) => {
const dt = Math.min(now - last, 50) / 1000;
context.clearRect(0, 0, window.innerWidth, window.innerHeight);
live.current = live.current.filter((p) => (p.age += dt) < p.life);
for (const p of live.current) {
p.vy += GRAVITY * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
// Fade out over the last 40% of each particle's life.
context.globalAlpha = Math.min(1, (p.life - p.age) / (p.life * 0.4));
context.fillText(p.glyph, p.x, p.y);
}
if (live.current.length) step(context, now);
else frame.current = 0;
});
}
function burst(x: number, y: number, amount: number) {
const canvas = canvasRef.current;
const context = canvas?.getContext('2d');
if (reduceMotion || !canvas || !context) return;
if (!frame.current) {
// Size the canvas as each loop starts, sharp on dense screens.
const ratio = window.devicePixelRatio;
canvas.width = window.innerWidth * ratio;
canvas.height = window.innerHeight * ratio;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.font = '22px system-ui';
context.textAlign = 'center';
step(context, performance.now());
}
for (let i = 0; i < amount; i++) {
// Up, within 45° of vertical, at 280 to 720px/s.
const angle = -Math.PI / 2 + (Math.random() - 0.5) * (Math.PI / 2);
const speed = 280 + Math.random() * 440;
const [vx, vy] = [Math.cos(angle) * speed, Math.sin(angle) * speed];
const glyph = GLYPHS[Math.floor(Math.random() * GLYPHS.length)] ?? '✨';
const life = 1 + Math.random() * 0.7;
live.current.push({ x, y, vx, vy, age: 0, life, glyph });
}
}
return (
<>
<button
type='button'
onPointerDown={(event) => {
onCheer();
burst(event.clientX, event.clientY, 16);
}}
className='rounded-full bg-foreground px-6 py-3 font-semibold text-background select-none active:scale-95'
>
Cheer <span className='tabular-nums opacity-70'>{count}</span>
</button>
<canvas
ref={canvasRef}
aria-hidden
className='pointer-events-none fixed inset-0 z-50 size-full'
/>
</>
);
}Add a particle burst to the "Cheer" button: on pointer down, spawn 16 emoji particles (🎉 ✨ 🚀 💖) at the pointer on a full-screen canvas overlay with pointer-events: none. Give each a random upward angle within 45 degrees of vertical, a speed of 280 to 720px/s, and a life of 1 to 1.7s; every frame, apply 1100px/s² of gravity and fade it out over the last 40% of its life. While the button is held, add 1 to the count and emit 4 more particles every 160ms. Cap the total at 360, stop the animation loop when no particles are left, and skip the particles when prefers-reduced-motion is set.
Paste into Layerwise or any AI coding tool.
What is Particle burst?
A particle burst is a tiny physics effect tied to a press. Each click spawns a handful of particles at the pointer and gives each a random upward speed and spin. Every frame after that, gravity pulls them down, they drift sideways a little less, and they fade out. Holding the button keeps a stream coming, which pairs well with counters you can hold to keep adding, like claps or cheers.
Use it for small, happy actions people repeat: likes, kudos, reactions, saving to a wishlist, or a playful button on a landing page. Keep each burst small, around 10 to 20 particles that live about a second, so the page stays usable while people keep clicking.
Draw all the particles on one <canvas> overlay with pointer-events: none, so the page never adds an element per particle, and run the requestAnimationFrame loop only while particles are alive. Cap the total for long holds, scale the canvas for devicePixelRatio, and make sure the button’s own press state and count work without the effect. Skip the particles for people who prefer reduced motion.
Often confused with
- Confetti
- Confetti is a one-time celebration that rains over the screen after a milestone. A particle burst comes from the pointer on every click, as part of the button’s feedback.
Related terms
All termsConfetti
A burst of small colored pieces that fly out and flutter down to celebrate a moment, such as finishing signup or publishing a site.
Ripple effect
A circle that spreads out from the exact spot you tap or click on a button and then fades, made popular by Google's Material Design.
Press feedback
A quick visual response, usually a slight shrink, that shows a button registered the press the moment a finger or mouse button goes down.
Hold to confirm
A button you press and hold for a moment, with a fill that shows progress, used to confirm a destructive or important action.
Build it with Layerwise
Describe your app, watch it run in a live preview, and publish it with a database, storage, and sign-in built in.