Skip to content
party.js logoparty.js logo

Guides

Create a custom effect

Build your own effects with createEffect(), from a single burst to a toggleable fountain.

confetti() and sparkles() are built on the same function you can use yourself: createEffect(). This guide builds a custom effect step by step, from a few dots at the cursor position to a fountain of particles that can be toggled on and off. It assumes you know the parts of an effect, and how samplers produce values for each particle.

An effect needs to know where it happens, when to emit particles, and what they look like. Everything else is optional:

Live Demo

Click anywhere
import { cone, createEffect, range } from "party-js";
createEffect({
origin: { x: event.pageX, y: event.pageY },
schedule: { bursts: [{ count: 20 }] },
particleInit: {
velocity: cone(90, 180, range(100, 300)),
size: 8,
color: "#ff5a5f",
shape: { type: "circle" },
lifetime: 1,
},
});
  • origin is where the effect happens, here the point you clicked. On the default fullscreen canvas, it’s in page coordinates, the same ones event.pageX and event.pageY use.
  • schedule emits a single burst of 20 particles, right when the effect starts.
  • particleInit describes each particle as it spawns. size, color, shape and lifetime are required. Here, the particles also get a velocity: cone() points them upwards, with a deviation of 180 degrees to either side, so in every direction.

The position, orientation and spin of a particle are optional. Without them, particles start at the origin, face the viewer, and don’t spin.

Most effects belong to an element, like a button that was clicked. measureTarget() measures an element and returns two things: its center as the origin, and its area to emit from. Passed to emitFrom(), the area spreads the particles over the whole element.

From there, a few more samplers and behaviors make the particles tumble and fall:

Live Demo

import { cone, createEffect, emitFrom, fade, gravity, measureTarget, pick, randomOrientation, randomSpin, range, scale } from "party-js";
const { origin, area } = measureTarget(button);
createEffect({
origin,
schedule: { bursts: [{ count: 30 }] },
particleInit: {
position: emitFrom(area),
velocity: cone(90, 30, range(300, 600)),
orientation: randomOrientation(),
angularVelocity: randomSpin(90, 360),
size: range(8, 12),
color: pick(["#7dd3fc", "#38bdf8", "#0ea5e9"]),
shape: { type: "square", aspectRatio: 0.5 },
lifetime: range(1.5, 2.5),
},
behaviors: [gravity(800), scale(0.1, 0), fade(0, 0.5)],
});
  • randomOrientation() and randomSpin() turn each particle in a random direction, and spin it around a random axis. That’s what makes the flat shapes look three-dimensional.
  • gravity() pulls the particles down, scale() grows them in quickly, and fade() fades them out at the end of their life.

measureTarget() measures the element once, when it’s called. If the page scrolls afterwards, the particles stay where they were on the page, like the built-in effects do.

A burst emits all of its particles at once. A rate emits them continuously instead, a number of particles per second. Together with loops: Infinity, the schedule never ends, so the effect runs until you end it:

Live Demo

import { createEffect, measureTarget } from "party-js";
let fountain;
// the first click starts the fountain, the second one turns it off and lets the last drops fall
if (fountain) {
fountain.end();
fountain = undefined;
} else {
const { origin, area } = measureTarget(button);
fountain = createEffect({
origin,
schedule: { rate: 40, loops: Infinity },
// ...
});
}

createEffect() returns a handle to the running effect, the same one the built-in effects return. Its end() method stops the fountain from emitting, and lets the particles that are already in the air finish their flight. Once the last one is gone, the effect ends by itself. To remove the particles right away instead, use stop().

The schedule can do more: several bursts at different times, repeated loops, or a rate that changes while the effect runs.

To use your effect like a built-in one, wrap it in a function that takes a target and options, and returns the effect:

import { createEffect, measureTarget, emitFrom, pick } from "party-js";
export function fountain(target, options = {}) {
const { rate = 40, colors = ["#7dd3fc", "#38bdf8", "#0ea5e9"] } = options;
const { origin, area } = measureTarget(target);
return createEffect({
origin,
schedule: { rate, loops: Infinity },
particleInit: {
position: emitFrom(area),
color: pick(colors),
// ...
},
// ...
});
}
const effect = fountain(button, { rate: 60 });

measureTarget() accepts an element or a point, just like confetti(), so fountain() does too. Returning the effect lets the caller pause it, stop it, or wait for it to end with its finished promise.

The origin can also be a function. party.js calls it again on every frame, so the effect can follow an element that moves:

createEffect({
origin: () => measureTarget(badge).origin,
// ...
});

All particles are positioned relative to the origin, so when it moves, the whole effect moves with it, including the particles already in the air. That fits effects that belong to an element, like sparkles around a badge that slides in. It doesn’t leave a trail behind a moving element.

Measuring an element on every frame isn’t free, so use this for effects that need it, not for every burst.

By default, effects draw on a fullscreen canvas that party.js manages for you. To draw somewhere else, for example behind your content, pass your own canvas. Its coordinates start at its top-left corner, so pass the canvas to measureTarget() as well, and it converts the origin for you:

const canvas = document.querySelector("#background");
const { origin, area } = measureTarget(button, canvas);
createEffect({
canvas,
origin,
particleInit: {
position: emitFrom(area),
// ...
},
// ...
});
  • Effects need a browser. createEffect() throws a PartyJSError during server-side rendering. Start effects from event handlers, or after your component has mounted.
  • Reduced motion is respected. When the user has asked their system for reduced motion, custom effects are skipped like the built-in ones. If your particles carry information the user would otherwise miss, pass reducedMotion: "play".
  • Seeds make effects repeatable. Pass a seed, and the effect looks the same every time. See Reproducible randomness.