Back

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.

PropTypeDefaultDescription
particleCountnumber8Number of particles created per click
particleColorsstring[]["#FF785A"]Colors picked randomly for each particle
particleSizeRange[number, number][1.5, 2.5]Min and max size of particles in px
velocityMinnumber0.5Minimum speed of particle velocity
velocityMaxnumber1.5Maximum speed of particle velocity
gravitynumber0.05Gravity applied to vertical velocity each frame
fadeSpeednumber0.02How quickly the particles fade out
angleRange[number, number][0, 360]Launch angle range in degrees
lifetimenumber1000Duration before particle disappears (ms)
childrenReactNodenullChildren to render inside the ClickParticles component. Only applicable for particle wrapper component
classNamestring""Class name to apply to the particle wrapper

Made by Dhvanit. Visit my portfolio.