Click Particles
A React component for creating clickable particles.
Click here
Component for whole screen
Create a file called ClickParticles.tsx
with this snippet and import it in your component.
"use client";
import { useEffect, useRef, useState } from "react";
const ClickParticles = ({
particleCount = 8,
particleColors = ["#FF785A"],
particleSizeRange = [1.5, 2.5],
velocityMin = 0.5,
velocityMax = 1.5,
gravity = 0.05,
fadeSpeed = 0.02,
angleRange = [0, 360],
lifetime = 1000,
}: ClickParticlesProps) => {
const [particles, setParticles] = useState<Particle[]>([]);
const particlesRef = useRef<Particle[]>([]);
const particleId = useRef(0);
const randomBetween = (min: number, max: number) =>
Math.random() * (max - min) + min;
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const x = e.clientX;
const y = e.clientY;
const newParticles = Array.from({ length: particleCount }, () => {
const angleDeg = randomBetween(angleRange[0], angleRange[1]);
const angleRad = (angleDeg * Math.PI) / 180;
const speed = randomBetween(velocityMin, velocityMax);
return {
x,
y,
vx: Math.cos(angleRad) * speed,
vy: Math.sin(angleRad) * speed,
id: particleId.current++,
opacity: 1,
size: randomBetween(particleSizeRange[0], particleSizeRange[1]),
color: particleColors[Math.floor(Math.random() * particleColors.length)],
createdAt: performance.now(),
};
});
particlesRef.current = [...particlesRef.current, ...newParticles];
setParticles(particlesRef.current);
};
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
}, [particleCount, angleRange, velocityMin, velocityMax, particleColors, particleSizeRange]);
useEffect(() => {
let animationFrameId: number;
const updateParticles = () => {
const now = performance.now();
particlesRef.current = particlesRef.current
.map(p => {
const age = now - p.createdAt;
const lifeProgress = age / lifetime;
return {
...p,
x: p.x + p.vx,
y: p.y + p.vy,
vy: p.vy + gravity,
opacity: 1 - lifeProgress,
};
})
.filter(p => now - p.createdAt < lifetime);
setParticles([...particlesRef.current]);
animationFrameId = requestAnimationFrame(updateParticles);
};
animationFrameId = requestAnimationFrame(updateParticles);
return () => cancelAnimationFrame(animationFrameId);
}, [gravity, fadeSpeed, lifetime]);
return (
<div
className={"fixed inset-0 pointer-events-none z-50"}
>
{particles.map(p => (
<div
key={p.id}
className="absolute rounded-full"
style={{
width: `${p.size}px`,
height: `${p.size}px`,
backgroundColor: p.color,
transform: `translate(${p.x}px, ${p.y}px)`,
opacity: p.opacity,
}}
/>
))}
</div>
);
};
export default ClickParticles;
Component for a specific area
Create a file called ClickParticles.tsx
with this snippet and import it in your component.
Pass a children component as a prop.
"use client";
import { useEffect, useRef, useState } from "react";
const ClickParticles = ({
children,
particleCount = 8,
particleColors = ["#FF785A"],
particleSizeRange = [1.5, 2.5],
velocityMin = 0.5,
velocityMax = 1.5,
gravity = 0.05,
fadeSpeed = 0.02,
angleRange = [0, 360],
lifetime = 1000,
className = "",
}: ClickParticlesProps) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const [particles, setParticles] = useState<Particle[]>([]);
const particlesRef = useRef<Particle[]>([]);
const particleId = useRef(0);
const randomBetween = (min: number, max: number) =>
Math.random() * (max - min) + min;
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = wrapperRef.current?.getBoundingClientRect();
if (!rect) return;
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const newParticles = Array.from({ length: particleCount }, () => {
const angleDeg = randomBetween(angleRange[0], angleRange[1]);
const angleRad = (angleDeg * Math.PI) / 180;
const speed = randomBetween(velocityMin, velocityMax);
return {
x,
y,
vx: Math.cos(angleRad) * speed,
vy: Math.sin(angleRad) * speed,
id: particleId.current++,
opacity: 1,
size: randomBetween(particleSizeRange[0], particleSizeRange[1]),
color: particleColors[Math.floor(Math.random() * particleColors.length)],
createdAt: performance.now(),
};
});
particlesRef.current = [...particlesRef.current, ...newParticles];
setParticles([...particlesRef.current]);
};
useEffect(() => {
let animationFrameId: number;
const updateParticles = () => {
const now = performance.now();
particlesRef.current = particlesRef.current
.map(p => {
const age = now - p.createdAt;
const lifeProgress = age / lifetime;
return {
...p,
x: p.x + p.vx,
y: p.y + p.vy,
vy: p.vy + gravity,
opacity: 1 - lifeProgress,
};
})
.filter(p => now - p.createdAt < lifetime);
setParticles([...particlesRef.current]);
animationFrameId = requestAnimationFrame(updateParticles);
};
animationFrameId = requestAnimationFrame(updateParticles);
return () => cancelAnimationFrame(animationFrameId);
}, [gravity, fadeSpeed, lifetime]);
return (
<div
ref={wrapperRef}
className={`relative ${className}`}
onClick={handleClick}
>
{children}
<div className="absolute inset-0 pointer-events-none z-50">
{particles.map(p => (
<div
key={p.id}
className="absolute rounded-full"
style={{
width: `${p.size}px`,
height: `${p.size}px`,
backgroundColor: p.color,
transform: `translate(${p.x}px, ${p.y}px)`,
opacity: p.opacity,
}}
/>
))}
</div>
</div>
);
};
export default ClickParticles;
Types
Paste these snippets into your component file. This one is particle interface.
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
id: number;
opacity: number;
size: number;
color: string;
createdAt: number;
};
And this is the props interface
interface ClickParticlesProps {
particleCount?: number;
particleColors?: string[]; // choose randomly
particleSizeRange?: [number, number]; // min, max px
velocityMin?: number;
velocityMax?: number;
gravity?: number;
fadeSpeed?: number;
angleRange?: [number, number]; // in degrees
lifetime?: number; // in milliseconds
className?: string; // only applicable for particle wrapper component
children?: React.ReactNode; // only applicable for particle wrapper component
};
Props
These are the props for the component. Customize them according to your needs.
Prop | Type | Default | Description |
---|---|---|---|
particleCount | number | 8 | Number of particles created per click |
particleColors | string[] | ["#FF785A"] | Colors picked randomly for each particle |
particleSizeRange | [number, number] | [1.5, 2.5] | Min and max size of particles in px |
velocityMin | number | 0.5 | Minimum speed of particle velocity |
velocityMax | number | 1.5 | Maximum speed of particle velocity |
gravity | number | 0.05 | Gravity applied to vertical velocity each frame |
fadeSpeed | number | 0.02 | How quickly the particles fade out |
angleRange | [number, number] | [0, 360] | Launch angle range in degrees |
lifetime | number | 1000 | Duration before particle disappears (ms) |
children | ReactNode | null | Children to render inside the ClickParticles component. Only applicable for particle wrapper component |
className | string | "" | Class name to apply to the particle wrapper |
Made by Dhvanit. Visit my portfolio.