Nuxt Integration
This guide covers integrating tsParticles into a Nuxt 3 (and Nuxt 4) project using the official @tsparticles/vue3 wrapper. Nuxt runs both server-side and client-side, so you must guard particle components against SSR.
Installation
Install the Vue 3 wrapper and the engine bundle of your choice:
npm install @tsparticles/vue3 tsparticlesFor a smaller bundle, install @tsparticles/slim instead of tsparticles:
npm install @tsparticles/vue3 @tsparticles/slimBasic Usage
Nuxt renders components on the server by default. Since tsParticles needs the browser canvas API, you must wrap the <vue-particles> component in a <client-only> tag:
<template>
<div class="page">
<client-only>
<vue-particles id="tsparticles" :options="options" @particles-loaded="particlesLoaded" />
</client-only>
<h1>My Nuxt App</h1>
</div>
</template>
<script setup lang="ts">
import type { ISourceOptions, Container } from "@tsparticles/engine";
const options: ISourceOptions = {
fullScreen: {
zIndex: -1,
},
background: {
color: "#0d47a1",
},
particles: {
number: { value: 80 },
links: { enable: true, color: "#ffffff" },
move: { enable: true },
size: { value: 3 },
},
};
const particlesLoaded = (container?: Container) => {
console.log("Particles container ready", container?.id);
};
</script>
<style scoped>
.page {
position: relative;
}
</style>The <client-only> wrapper ensures the <vue-particles> component is only mounted in the browser, preventing hydration mismatches.
Configuration
Use the full ISourceOptions type for type-safe configuration. You can define your options inline or import them from a separate config file:
<script setup lang="ts">
import type { ISourceOptions } from "@tsparticles/engine";
const options: ISourceOptions = {
fpsLimit: 60,
background: {
color: "#000000",
},
particles: {
number: {
value: 100,
density: {
enable: true,
},
},
color: {
value: ["#ff0000", "#00ff00", "#0000ff"],
},
shape: {
type: ["circle", "square", "triangle"],
},
opacity: {
value: 0.8,
},
size: {
value: { min: 1, max: 8 },
},
links: {
enable: true,
distance: 150,
color: "#ffffff",
opacity: 0.4,
width: 1,
},
move: {
enable: true,
speed: 3,
direction: "none",
random: false,
straight: false,
outModes: "bounce",
},
},
interactivity: {
events: {
onHover: {
enable: true,
mode: "repulse",
},
onClick: {
enable: true,
mode: "push",
},
},
},
};
</script>Snow Effect
Create a wintery snowfall effect using the snow preset:
npm install @tsparticles/preset-snow<template>
<client-only>
<vue-particles id="snow" :options="options" @particles-loaded="onLoad" />
</client-only>
</template>
<script setup lang="ts">
import { loadSnowPreset } from "@tsparticles/preset-snow";
import { tsParticles } from "@tsparticles/engine";
import type { Container } from "@tsparticles/engine";
// Load the preset before the component mounts
await loadSnowPreset(tsParticles);
const options = {
preset: "snow",
fullScreen: { zIndex: -1 },
background: {
color: "#1a1a2e",
},
};
const onLoad = (container?: Container) => {
console.log("Snow effect ready", container?.id);
};
</script>Because the preset is loaded with top-level await in the <script setup>, it is guaranteed to be ready before the component renders.
Interactive Particles
Enable click and hover interactions by adding interactivity modes:
<template>
<client-only>
<vue-particles id="interactive" :options="options" />
</client-only>
</template>
<script setup lang="ts">
import type { ISourceOptions } from "@tsparticles/engine";
const options: ISourceOptions = {
fullScreen: { zIndex: -1 },
particles: {
number: { value: 50 },
links: {
enable: true,
distance: 150,
},
move: {
enable: true,
speed: 2,
},
size: {
value: { min: 1, max: 4 },
},
},
interactivity: {
events: {
onHover: {
enable: true,
mode: "grab", // particles connect to the cursor
},
onClick: {
enable: true,
mode: "push", // add particles on click
},
},
modes: {
grab: {
distance: 200,
links: {
opacity: 0.5,
},
},
push: {
quantity: 4,
},
},
},
};
</script>Available interaction modes include: grab, bubble, connect, repulse, push, remove, attract, and slow.
Event Handling
The <vue-particles> component emits several lifecycle events:
<template>
<client-only>
<vue-particles
id="event-demo"
:options="options"
@particles-loaded="onLoaded"
@particles-init="onInit"
@particles-destroy="onDestroy"
/>
</client-only>
</template>
<script setup lang="ts">
import type { Container, Engine } from "@tsparticles/engine";
const options = {
fullScreen: { zIndex: -1 },
particles: {
number: { value: 60 },
links: { enable: true },
move: { enable: true },
},
};
const onInit = (engine: Engine) => {
console.log("Engine initialized", engine);
};
const onLoaded = (container: Container) => {
console.log("Container loaded", container.id);
};
const onDestroy = () => {
console.log("Container destroyed");
};
</script>| Event | Payload | Description |
|---|---|---|
@particles-init | Engine | Fires once when the tsParticles engine initializes |
@particles-loaded | Container | Fires every time the container finishes loading or reloading |
@particles-destroy | none | Fires when the container is destroyed |
Full TypeScript Example
A complete, typed component with explicit imports and lifecycle awareness:
<template>
<div class="particles-wrapper">
<client-only>
<vue-particles
id="full-example"
:options="options"
@particles-loaded="onParticlesLoaded"
@particles-init="onParticlesInit"
/>
</client-only>
<div class="controls">
<button @click="togglePause">{{ paused ? "Resume" : "Pause" }}</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { loadFull } from "tsparticles";
import type { Container, Engine, ISourceOptions } from "@tsparticles/engine";
const containerRef = ref<Container | undefined>(undefined);
const paused = ref(false);
const options: ISourceOptions = {
fullScreen: { zIndex: -1 },
background: { color: "#0a0a23" },
particles: {
color: { value: "#00ff00" },
number: { value: 80 },
links: { enable: true, color: "#00ff00", distance: 150 },
move: { enable: true, speed: 1.5 },
size: { value: { min: 1, max: 4 } },
},
interactivity: {
events: {
onHover: { enable: true, mode: "repulse" },
},
modes: {
repulse: { distance: 120 },
},
},
};
const onParticlesInit = async (engine: Engine) => {
await loadFull(engine);
};
const onParticlesLoaded = (container: Container) => {
containerRef.value = container;
};
const togglePause = () => {
if (containerRef.value) {
if (paused.value) {
containerRef.value.play();
} else {
containerRef.value.pause();
}
paused.value = !paused.value;
}
};
</script>
<style scoped>
.particles-wrapper {
position: relative;
min-height: 100vh;
}
.controls {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 10;
}
</style>Page Integration
Add a particle background to a specific Nuxt page by placing the component in the page's template:
<template>
<div>
<client-only>
<vue-particles id="page-particles" :options="options" />
</client-only>
<div class="content">
<h1>About Page</h1>
<p>This content sits above the particle canvas.</p>
</div>
</div>
</template>
<script setup lang="ts">
import type { ISourceOptions } from "@tsparticles/engine";
const options: ISourceOptions = {
fullScreen: { zIndex: -1 },
background: { color: "#1a1a2e" },
particles: {
number: { value: 50 },
color: { value: "#e94560" },
links: { enable: true, color: "#e94560" },
move: { enable: true },
},
};
</script>
<style scoped>
.content {
position: relative;
z-index: 1;
padding: 2rem;
color: white;
}
</style>If you want particles on every page, add the component to layouts/default.vue instead of individual pages.
Nuxt 4 Notes
Nuxt 4 maintains backward compatibility with Nuxt 3's <client-only> and <script setup> patterns. All of the examples above work without changes in Nuxt 4.
Key considerations for Nuxt 4:
- Nitropack 2: The server engine is upgraded, but it does not affect client-only components like
<vue-particles>. - Vue 3.5+: Nuxt 4 ships with a newer Vue version —
@tsparticles/vue3is compatible with Vue 3.3+ without issues. - Stricter SSR checks: If you see hydration warnings, ensure
<vue-particles>is always inside<client-only>and never rendered on the server. - Hybrid rendering: If using route rules with
ssr: falsefor certain pages, you can omit<client-only>on those pages, but it is safer to always include it.
If you upgrade from Nuxt 2 with the @tsparticles/vue package (vue 2), you must migrate to @tsparticles/vue3 for Nuxt 3 / 4 — the APIs are not compatible.
Preset Gallery
Combine the pattern above with any of these official presets:
| Preset | Package | Effect |
|---|---|---|
| Confetti | @tsparticles/preset-confetti | Colorful confetti burst |
| Fireworks | @tsparticles/preset-fireworks | Firework explosions |
| Snow | @tsparticles/preset-snow | Falling snowflakes |
| Stars | @tsparticles/preset-stars | Twinkling night sky |
| Links | @tsparticles/preset-links | Connected node network |
| Bubbles | @tsparticles/preset-bubbles | Floating bubbles |
<template>
<client-only>
<vue-particles id="preset-demo" :options="{ preset: 'stars' }" />
</client-only>
</template>
<script setup lang="ts">
import { loadStarsPreset } from "@tsparticles/preset-stars";
import { tsParticles } from "@tsparticles/engine";
await loadStarsPreset(tsParticles);
</script>Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Blank screen / hydration error | <vue-particles> rendered on the server | Wrap in <client-only> |
| Preset has no effect | Preset not loaded before component mount | Call loadXPreset() with top-level await in <script setup> |
| Canvas does not fill the viewport | fullScreen not enabled | Add fullScreen: { zIndex: -1 } to the options |
| Controls do not pause/resume | Container ref not set | Assign the container in the @particles-loaded handler |
Next Steps
- Explore the Interactive Demos for ready-made Vue configurations.
- Read the Options Reference for a complete list of particle parameters.
- Visit the Presets page for more pre-built effects.
