Implementation of Particles.js / tsParticles Effects on Website
tsParticles is the successor to the original Particles.js with active support, TypeScript types, and significantly broader capabilities. The original Particles.js has been abandoned since 2016. If the project is new — use tsParticles. It provides: particles, confetti, fireworks, snow, bubbles, links between particles, mouse interactivity.
Installation (Modular, Only Necessary Presets)
# Minimal installation — core + basic elements
npm install @tsparticles/react @tsparticles/engine @tsparticles/slim
# Or full — all presets (~180 KB gzip)
npm install @tsparticles/react @tsparticles/all
Modular installation is preferable — @tsparticles/slim weighs ~40 KB gzip vs ~180 KB for @tsparticles/all.
Basic Integration with React
// components/ParticlesBackground.tsx
'use client'
import { useEffect, useState, useCallback } from 'react'
import Particles, { initParticlesEngine } from '@tsparticles/react'
import { loadSlim } from '@tsparticles/slim'
import type { ISourceOptions } from '@tsparticles/engine'
const particleOptions: ISourceOptions = {
background: {
color: { value: 'transparent' },
},
fpsLimit: 60,
interactivity: {
events: {
onHover: {
enable: true,
mode: 'repulse', // repel from cursor
},
onClick: {
enable: true,
mode: 'push', // add particles on click
},
},
modes: {
repulse: { distance: 100, duration: 0.4 },
push: { quantity: 4 },
},
},
particles: {
color: { value: '#3b82f6' },
links: {
color: '#3b82f6',
distance: 150,
enable: true,
opacity: 0.3,
width: 1,
},
move: {
enable: true,
speed: 1.5,
direction: 'none',
random: false,
straight: false,
outModes: { default: 'bounce' },
},
number: {
value: 60,
density: { enable: true, area: 800 },
},
opacity: { value: 0.5 },
shape: { type: 'circle' },
size: { value: { min: 1, max: 3 } },
},
detectRetina: true,
}
export function ParticlesBackground() {
const [engineReady, setEngineReady] = useState(false)
useEffect(() => {
initParticlesEngine(async (engine) => {
await loadSlim(engine)
}).then(() => setEngineReady(true))
}, [])
if (!engineReady) return null
return (
<Particles
id="tsparticles"
options={particleOptions}
className="absolute inset-0 -z-10"
/>
)
}
Preset: Connected Network
// presets/network.ts
import type { ISourceOptions } from '@tsparticles/engine'
export const networkPreset: ISourceOptions = {
fpsLimit: 60,
particles: {
number: { value: 80, density: { enable: true, area: 1000 } },
color: { value: ['#3b82f6', '#8b5cf6', '#06b6d4'] },
shape: { type: 'circle' },
opacity: {
value: { min: 0.3, max: 0.8 },
animation: { enable: true, speed: 1, minimumValue: 0.1 },
},
size: {
value: { min: 1, max: 4 },
animation: { enable: true, speed: 2, minimumValue: 0.5 },
},
links: {
enable: true,
distance: 120,
color: { value: '#94a3b8' },
opacity: 0.2,
width: 1,
triangles: {
enable: false,
},
},
move: {
enable: true,
speed: { min: 0.5, max: 1.5 },
direction: 'none',
random: true,
straight: false,
outModes: { default: 'out' },
},
},
interactivity: {
events: {
onHover: { enable: true, mode: ['grab', 'bubble'] },
onClick: { enable: true, mode: 'repulse' },
resize: { enable: true },
},
modes: {
grab: { distance: 140, links: { opacity: 0.8 } },
bubble: { distance: 100, size: 8, duration: 0.3, opacity: 0.8 },
repulse: { distance: 150, duration: 0.4 },
},
},
detectRetina: true,
}
Confetti on Event (Success, Form Submitted)
// hooks/useConfetti.ts
import { useCallback } from 'react'
import { tsParticles } from '@tsparticles/engine'
export function useConfetti() {
const fire = useCallback(async (originX = 0.5, originY = 0.6) => {
await tsParticles.load({
id: 'confetti-' + Date.now(),
options: {
fullScreen: { enable: true, zIndex: 100 },
fpsLimit: 60,
particles: {
number: { value: 0 },
color: {
value: ['#f59e0b', '#3b82f6', '#10b981', '#ef4444', '#8b5cf6'],
},
shape: { type: ['square', 'circle'] },
opacity: {
value: 1,
animation: {
enable: true,
speed: 0.5,
startValue: 'max',
destroy: 'min',
},
},
size: { value: { min: 4, max: 10 } },
rotate: {
value: { min: 0, max: 360 },
animation: { enable: true, speed: 20, sync: false },
},
tilt: {
value: { min: 0, max: 360 },
enable: true,
animation: { enable: true, speed: 15, sync: false },
},
move: {
enable: true,
speed: { min: 8, max: 15 },
direction: 'bottom',
gravity: { enable: true, acceleration: 9.8 },
drift: { min: -2, max: 2 },
decay: { min: 0.02, max: 0.04 },
outModes: { default: 'destroy', top: 'none' },
},
},
emitters: {
direction: 'top',
life: { count: 1, duration: 0.1, delay: 0 },
rate: { delay: 0, quantity: 150 },
size: { width: 0, height: 0 },
position: { x: originX * 100, y: originY * 100 },
},
},
})
}, [])
return { fire }
}
// Usage
const { fire } = useConfetti()
const handleFormSubmit = async () => {
await submitForm()
fire() // trigger confetti after success
}
Performance and Disabling on Mobile
// components/ParticlesBackground.tsx (with responsive)
import { useEffect, useState } from 'react'
export function ParticlesBackground() {
const [shouldRender, setShouldRender] = useState(false)
useEffect(() => {
// Don't render on weak devices and with prefers-reduced-motion
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const isMobile = window.innerWidth < 768
const isLowEndDevice = navigator.hardwareConcurrency <= 2
setShouldRender(!reducedMotion && !isMobile && !isLowEndDevice)
}, [])
if (!shouldRender) return null
return <ParticlesCore />
}
Typical Timeframes
Basic particle background — 3–4 hours. Multiple presets with custom settings, confetti, responsive disabling and performance optimization — 1–2 business days.







