Stencil Guide
Table of Contents
- Installation
- Custom Elements Registration
- Basic Usage
- Engine Initialization
- Custom Configuration
- Component Lifecycle
- TypeScript Example
Installation
Install the Stencil wrapper and the tsParticles engine via npm:
npm install @tsparticles/stencil tsparticlesOptionally install a preset bundle to reduce manual configuration:
npm install @tsparticles/slimCustom Elements Registration
The @tsparticles/stencil package provides a defineCustomElements function that registers the <stencil-particles> custom element with the browser. Call it once before using the component anywhere in your app.
import { defineCustomElements } from "@tsparticles/stencil/loader";
// Register the <stencil-particles> element
defineCustomElements();For Stencil projects using lazy-loading, call this inside componentWillLoad or in your app's root component to ensure the element is available before rendering.
Basic Usage
Once the custom element is registered, you can use <stencil-particles> in your JSX with an options prop and an init callback to load the required engine features.
import { Component, h } from "@stencil/core";
import type { ISourceOptions } from "@tsparticles/engine";
import { loadSlim } from "@tsparticles/slim";
const options: ISourceOptions = {
fpsLimit: 60,
particles: {
number: { value: 80 },
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,
width: 1,
},
move: {
enable: true,
speed: 1.5,
direction: "none",
random: true,
outModes: { default: "bounce" },
},
},
background: { color: "#0d1117" },
};
@Component({ tag: "my-particles" })
export class MyParticles {
render() {
return (
<stencil-particles
container-id="tsparticles"
options={options}
init={async (engine) => {
await loadSlim(engine);
}}
/>
);
}
}Engine Initialization
The init prop receives the engine instance and lets you load the features you need. This is the recommended place to call loadSlim, loadFull, or individual updater/interaction plugins.
import { loadSlim } from "@tsparticles/slim";
import { loadFull } from "tsparticles";
import { loadConfettiPreset } from "@tsparticles/preset-confetti";
// Option A: lightweight (circles, basic movement, links)
init={async engine => { await loadSlim(engine); }}
// Option B: full feature set (all shapes, effects, presets)
init={async engine => { await loadFull(engine); }}
// Option C: presets (confetti, fireworks, snow, stars)
init={async engine => { await loadConfettiPreset(engine); }}The engine instance is also accessible after initialization through the container-id attribute, allowing you to programmatically control the particle system later if needed.
Custom Configuration
Below is a full configuration with interactivity, multiple shape types, and hover/click modes.
import { Component, h } from "@stencil/core";
import type { ISourceOptions } from "@tsparticles/engine";
import { loadSlim } from "@tsparticles/slim";
const fullOptions: ISourceOptions = {
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,
straight: false,
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",
},
};
@Component({ tag: "app-particles" })
export class AppParticles {
render() {
return (
<stencil-particles
container-id="tsparticles"
options={fullOptions}
init={async (engine) => {
await loadSlim(engine);
}}
/>
);
}
}Component Lifecycle
In Stencil, the recommended lifecycle hook for one-time setup is componentWillLoad. Use it to register custom elements and manage initialization state so that the <stencil-particles> component only renders when the engine is prepared.
import { Component, h, State } from "@stencil/core";
import type { ISourceOptions } from "@tsparticles/engine";
import { defineCustomElements } from "@tsparticles/stencil/loader";
import { loadSlim } from "@tsparticles/slim";
@Component({ tag: "app-root" })
export class AppRoot {
@State() private engineReady = false;
private options: ISourceOptions = {
particles: {
number: { value: 50 },
color: { value: "#ffffff" },
shape: { type: "circle" },
opacity: { value: 0.6 },
size: { value: { min: 1, max: 4 } },
move: {
enable: true,
speed: 1,
outModes: { default: "bounce" },
},
},
background: { color: "#1a1a2e" },
};
componentWillLoad() {
defineCustomElements();
this.engineReady = true;
}
render() {
return (
<div>
<h1>tsParticles + Stencil</h1>
{this.engineReady && (
<stencil-particles
container-id="tsparticles"
options={this.options}
init={async (engine) => {
await loadSlim(engine);
}}
/>
)}
</div>
);
}
}Using @State() ensures the component re-renders when the engine becomes ready, and the conditional render prevents the particles container from mounting before the custom element is defined.
TypeScript Example
Here is a complete, typed Stencil application component that integrates tsParticles with the slim preset, hover interactivity, and a custom dark theme.
import { Component, h, State, Prop } from "@stencil/core";
import type { Container, Engine, ISourceOptions } from "@tsparticles/engine";
import { defineCustomElements } from "@tsparticles/stencil/loader";
import { loadSlim } from "@tsparticles/slim";
@Component({
tag: "app-home",
styleUrl: "app-home.css",
shadow: true,
})
export class AppHome {
@State() private initialized = false;
@Prop() readonly title: string = "Welcome";
private container?: Container;
private readonly options: ISourceOptions = {
fullScreen: { enable: true, zIndex: -1 },
fpsLimit: 60,
particles: {
number: { value: 80, density: { enable: true } },
color: { value: "#6366f1" },
shape: { type: "circle" },
opacity: { value: { min: 0.3, max: 0.7 } },
size: { value: { min: 2, max: 6 } },
links: {
enable: true,
distance: 160,
color: "#6366f1",
opacity: 0.25,
width: 1,
},
move: {
enable: true,
speed: 1.2,
direction: "none",
random: false,
straight: false,
outModes: { default: "bounce" },
},
},
interactivity: {
events: {
onHover: { enable: true, mode: "grab" },
onClick: { enable: true, mode: "push" },
},
modes: {
grab: { distance: 180, links: { opacity: 0.6 } },
push: { quantity: 3 },
},
},
background: { color: "#0a0a1a" },
};
componentWillLoad() {
defineCustomElements();
this.initialized = true;
}
private handleInit = async (engine: Engine): Promise<void> => {
await loadSlim(engine);
};
private handleLoaded = async (container?: Container): Promise<void> => {
this.container = container;
console.log("Particles container loaded:", container?.id);
};
render() {
return (
<div class="home">
<h1>{this.title}</h1>
<p>Powered by tsParticles and Stencil</p>
{this.initialized && (
<stencil-particles
container-id="tsparticles"
options={this.options}
init={this.handleInit}
particlesLoaded={this.handleLoaded}
/>
)}
</div>
);
}
}The particlesLoaded event fires once the first frame is rendered, giving you access to the Container instance for programmatic control (play, pause, stop, switch themes).
You now have everything needed to integrate tsParticles into a Stencil application. Each example is self-contained and ready to be copied into your project.
