React Guide
Table of Contents
- Installation
- Basic Usage
- Confetti Effect
- Fireworks Effect
- Snow Effect
- Interactive Links
- Theme Switching
- ParticlesProvider
- Performance Optimization
- Custom Configuration
Installation
npm install @tsparticles/react tsparticlesOr with Yarn:
yarn add @tsparticles/react tsparticles@tsparticles/react is the official React wrapper. The tsparticles package is the core engine.
Basic Usage
The simplest setup: render the <Particles /> component with an options object.
import { useCallback } from "react";
import Particles from "@tsparticles/react";
export default function App() {
const particlesLoaded = useCallback(async (container) => {
console.log("Particles container loaded", container);
}, []);
const options = {
fpsLimit: 120,
particles: {
number: { value: 80 },
color: { value: "#00d4ff" },
shape: { type: "circle" },
opacity: { value: 0.6 },
size: { value: { min: 2, max: 5 } },
move: {
enable: true,
speed: 2,
outModes: { default: "bounce" },
},
},
background: { color: "#0d1117" },
};
return <Particles id="tsparticles" particlesLoaded={particlesLoaded} options={options} />;
}Important: The <Particles /> component requires the engine to be initialized first. Use initParticlesEngine from @tsparticles/react or the <ParticlesProvider> to load your presets before rendering the component.
Confetti Effect
Render a confetti burst using the confetti preset.
import Particles from "@tsparticles/react";
export default function Confetti() {
const options = {
preset: "confetti",
fullScreen: { enable: true, zIndex: -1 },
confetti: {
colors: ["#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff"],
particleCount: 150,
spread: 70,
origin: { x: 0.5, y: 0.5 },
},
};
return <Particles id="confetti" options={options} />;
}Make sure you have loaded the confetti preset:
npm install @tsparticles/preset-confettiThen register it in your app entry point:
import { initParticlesEngine } from "@tsparticles/react";
import { loadConfettiPreset } from "@tsparticles/preset-confetti";
initParticlesEngine(async (engine) => {
await loadConfettiPreset(engine);
});Fireworks Effect
A full-screen fireworks display.
import { useCallback, useMemo } from "react";
import Particles from "@tsparticles/react";
export default function Fireworks() {
const particlesLoaded = useCallback(async (container) => {
console.log("Fireworks loaded", container);
}, []);
const options = useMemo(
() => ({
preset: "fireworks",
fullScreen: { enable: true, zIndex: -1 },
fireworks: {
background: "#000000",
brightness: 100,
colors: ["#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff"],
intensity: 30,
life: { min: 4, max: 8 },
traces: 20,
explosion: { min: 30, max: 60 },
},
}),
[],
);
return <Particles id="fireworks" particlesLoaded={particlesLoaded} options={options} />;
}Install the preset:
npm install @tsparticles/preset-fireworksSnow Effect
Gentle falling snowflakes using the snow preset.
import { useCallback, useMemo } from "react";
import Particles from "@tsparticles/react";
export default function Snow() {
const particlesLoaded = useCallback(async (container) => {
console.log("Snow loaded", container);
}, []);
const options = useMemo(
() => ({
preset: "snow",
fullScreen: { enable: true, zIndex: -1 },
snow: {
color: "#ffffff",
opacity: { min: 0.3, max: 0.9 },
size: { min: 1, max: 4 },
speed: { min: 0.5, max: 2 },
wobble: true,
},
}),
[],
);
return <Particles id="snow" particlesLoaded={particlesLoaded} options={options} />;
}Install the preset:
npm install @tsparticles/preset-snowInteractive Links
A connected-nodes network with mouse hover grab and click push.
import { useCallback, useMemo } from "react";
import Particles from "@tsparticles/react";
export default function InteractiveLinks() {
const particlesLoaded = useCallback(async (container) => {
console.log("Interactive links loaded", container);
}, []);
const options = useMemo(
() => ({
fpsLimit: 60,
particles: {
number: { value: 80, density: { enable: true } },
color: { value: "#00d4ff" },
shape: { type: "circle" },
opacity: { value: 0.6 },
size: { value: { min: 1, max: 4 } },
links: {
enable: true,
distance: 150,
color: "#00d4ff",
opacity: 0.4,
width: 1,
},
move: {
enable: true,
speed: 1.5,
outModes: { default: "bounce" },
},
},
interactivity: {
events: {
onHover: { enable: true, mode: "grab" },
onClick: { enable: true, mode: "push" },
},
modes: {
grab: { distance: 180, links: { opacity: 0.8 } },
push: { quantity: 4 },
},
},
background: { color: "#0d1117" },
}),
[],
);
return <Particles id="interactive-links" particlesLoaded={particlesLoaded} options={options} />;
}Theme Switching
Define multiple themes and switch between them with a button click.
import { useCallback, useMemo, useRef, useState } from "react";
import Particles from "@tsparticles/react";
export default function ThemeSwitcher() {
const containerRef = useRef(null);
const [currentTheme, setCurrentTheme] = useState("dark");
const particlesLoaded = useCallback(async (container) => {
containerRef.current = container;
}, []);
const options = useMemo(
() => ({
fpsLimit: 60,
particles: {
number: { value: 60 },
color: { value: "#00d4ff" },
shape: { type: "circle" },
opacity: { value: 0.6 },
size: { value: { min: 2, max: 5 } },
links: { enable: true, distance: 150, color: "#00d4ff", opacity: 0.3 },
move: { enable: true, speed: 1.5, outModes: { default: "bounce" } },
},
background: { color: "#0d1117" },
themes: [
{
name: "dark",
default: { value: true },
options: {
background: { color: "#0d1117" },
particles: { color: { value: "#00d4ff" }, links: { color: "#00d4ff" } },
},
},
{
name: "light",
options: {
background: { color: "#f5f5f5" },
particles: { color: { value: "#e74c3c" }, links: { color: "#333333" } },
},
},
{
name: "forest",
options: {
background: { color: "#1a3a1a" },
particles: { color: { value: "#7ec850" }, links: { color: "#7ec850" } },
},
},
],
}),
[],
);
const switchTheme = useCallback((theme) => {
setCurrentTheme(theme);
if (containerRef.current) {
containerRef.current.loadTheme(theme);
}
}, []);
return (
<div style={{ position: "relative", width: "100%", height: "100vh" }}>
<Particles id="theme-switcher" particlesLoaded={particlesLoaded} options={options} />
<div
style={{
position: "absolute",
bottom: 20,
left: "50%",
transform: "translateX(-50%)",
zIndex: 10,
display: "flex",
gap: 8,
}}
>
<button onClick={() => switchTheme("dark")} style={btnStyle(currentTheme === "dark")}>
Dark
</button>
<button onClick={() => switchTheme("light")} style={btnStyle(currentTheme === "light")}>
Light
</button>
<button onClick={() => switchTheme("forest")} style={btnStyle(currentTheme === "forest")}>
Forest
</button>
</div>
</div>
);
}
const btnStyle = (active) => ({
padding: "8px 16px",
border: "none",
borderRadius: 6,
background: active ? "#333" : "#666",
color: "#fff",
cursor: "pointer",
});ParticlesProvider
Use ParticlesProvider to initialize the engine once at the app root. This is the recommended approach when you have multiple particle components or use custom presets.
// App.jsx
import { ParticlesProvider } from "@tsparticles/react";
import { loadSlim } from "@tsparticles/slim";
import Home from "./Home";
const engineInit = async (engine) => {
await loadSlim(engine);
// Load additional presets here:
// await loadConfettiPreset(engine);
// await loadFireworksPreset(engine);
// await loadSnowPreset(engine);
};
export default function App() {
return (
<ParticlesProvider load={engineInit}>
<Home />
</ParticlesProvider>
);
}// Home.jsx
import Particles from "@tsparticles/react";
export default function Home() {
return (
<Particles
id="tsparticles"
options={{
particles: {
number: { value: 50 },
color: { value: "#ff6b6b" },
shape: { type: "circle" },
opacity: { value: 0.7 },
size: { value: { min: 2, max: 6 } },
move: { enable: true, speed: 1, outModes: { default: "bounce" } },
},
background: { color: "#1a1a2e" },
}}
/>
);
}When wrapping your tree with ParticlesProvider, every child <Particles /> component inherits the same engine instance. This avoids re-initializing the engine on every mount.
Performance Optimization
Always memoize callbacks and options to prevent unnecessary re-renders.
import { useCallback, useMemo, useState } from "react";
import Particles from "@tsparticles/react";
export default function PerformanceExample() {
const [visible, setVisible] = useState(true);
// Memoize the callback — stable reference across renders
const particlesLoaded = useCallback(async (container) => {
// Called once per container mount
console.log("Container ready", container.id);
}, []);
// Memoize the options object — only recomputes when dependencies change
const options = useMemo(
() => ({
fpsLimit: 60,
particles: {
number: { value: 100, density: { enable: true } },
color: { value: ["#ff6b6b", "#feca57", "#48dbfb"] },
shape: { type: "circle" },
opacity: { value: { min: 0.3, max: 0.7 } },
size: { value: { min: 2, max: 5 } },
links: {
enable: true,
distance: 120,
color: "random",
opacity: 0.3,
},
move: {
enable: true,
speed: 1,
outModes: { default: "bounce" },
},
},
background: { color: "#0d1117" },
}),
[],
);
// Reduce canvas resolution on low-power devices
const responsiveOptions = useMemo(
() => ({
...options,
detectRetina: window.devicePixelRatio <= 2,
fpsLimit: window.innerWidth < 768 ? 30 : 60,
}),
[options],
);
return (
<div>
<button onClick={() => setVisible((v) => !v)}>{visible ? "Hide" : "Show"} Particles</button>
{visible && <Particles id="perf-particles" particlesLoaded={particlesLoaded} options={responsiveOptions} />}
</div>
);
}Key tips:
- Always use
useMemofor theoptionsobject. - Always use
useCallbackfor theparticlesLoadedhandler. - Lower
fpsLimiton mobile. - Set
detectRetina: falseon devices with >2x pixel ratio to halve canvas size. - Conditionally unmount
<Particles />when it is off-screen.
Custom Configuration
A full custom example combining multiple shapes, interactivity, themes, and a gradient background.
import { useCallback, useMemo } from "react";
import Particles from "@tsparticles/react";
export default function CustomConfig() {
const particlesLoaded = useCallback(async (container) => {
console.log("Custom config loaded", container);
}, []);
const options = useMemo(
() => ({
fullScreen: { enable: true, zIndex: 0 },
fpsLimit: 60,
particles: {
number: { value: 60, density: { enable: true, width: 800, height: 800 } },
color: {
value: ["#ff6b6b", "#feca57", "#48dbfb", "#ff9ff3", "#54a0ff"],
},
shape: {
type: ["circle", "triangle", "polygon"],
options: {
polygon: { sides: 6 },
},
},
opacity: { value: { min: 0.4, max: 0.8 } },
size: { value: { min: 3, max: 8 } },
links: {
enable: true,
distance: 200,
color: "#ffffff",
opacity: 0.15,
width: 1,
},
move: {
enable: true,
speed: 2,
direction: "none",
random: true,
outModes: { default: "out" },
},
},
interactivity: {
events: {
onHover: { enable: true, mode: "attract" },
onClick: { enable: true, mode: "repulse" },
},
modes: {
attract: { distance: 200, duration: 0.4, factor: 1 },
repulse: { distance: 200, duration: 0.4 },
},
},
background: { color: "#0f0f23" },
themes: [
{
name: "light",
default: { value: false },
options: {
background: { color: "#f0f0f5" },
particles: {
color: { value: ["#e74c3c", "#2ecc71", "#3498db", "#f1c40f"] },
links: { color: "#333333", opacity: 0.2 },
opacity: { value: { min: 0.5, max: 0.9 } },
},
},
},
],
}),
[],
);
return <Particles id="custom-config" particlesLoaded={particlesLoaded} options={options} />;
}You have now covered the core patterns for using tsParticles in React. Each example is self-contained and ready to drop into a component file.
