Реалізація Particles.js / tsParticles ефектів на сайті
tsParticles — наступник оригінального Particles.js з активною підтримкою, TypeScript-типами та значно ширшими можливостями. Оригінальний Particles.js заброшений з 2016 року. Якщо проект новий — використовуйте tsParticles. Він дає: частинки, конфеті, феєрверки, сніг, бульбашки, зв'язки між частинками, інтерактивність з мишею.
Установка (модульна, лише необхідні пресети)
# Мінімальна установка — ядро + базові елементи
npm install @tsparticles/react @tsparticles/engine @tsparticles/slim
# Або повна — усі пресети (~180 КБ gzip)
npm install @tsparticles/react @tsparticles/all
Модульна збірка переважніша — @tsparticles/slim важить ~40 КБ gzip проти ~180 КБ у @tsparticles/all.
Базова інтеграція з 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', // відтягувати від курсора
},
onClick: {
enable: true,
mode: 'push', // додавати частинки при кліку
},
},
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"
/>
)
}
Пресет: пов'язана мережа
// 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,
}
Конфеті при події (успіх, форма відправлена)
// 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 }
}
// Використання
const { fire } = useConfetti()
const handleFormSubmit = async () => {
await submitForm()
fire() // запускаємо конфеті після успіху
}
Продуктивність і відключення на мобільних
// components/ParticlesBackground.tsx (з адаптивом)
import { useEffect, useState } from 'react'
export function ParticlesBackground() {
const [shouldRender, setShouldRender] = useState(false)
useEffect(() => {
// Не рендеримо на слабких пристроях та при 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 />
}
Типові терміни
Базовий фон з частинками — 3–4 години. Кілька пресетів з кастомними налаштуваннями, конфеті, адаптивним відключенням та оптимізацією продуктивності — 1–2 робочих дні.







