Skip to content

Using motly with GSAP

Beside spec, vars takes four keys of motly’s: seed, container, renderer and reducedMotion, described below. Every other key means what it means in any GSAP tween, and motly’s own words for the burst live only in spec:

  • delay holds back the whole tween. spec.delay is a wait at the start of the burst, inside the tween’s length, as it is when the Spec plays without GSAP.
  • ease warps the burst’s time; spec.easing gives each property its curve. ease is 'none' unless you give one, so a Spec runs at its own pace rather than under GSAP’s default ease.
  • duration stretches or squeezes the whole burst. Without it, the tween lasts as long as the Spec.
  • repeat, yoyo, paused, onStart, onUpdate, onComplete and scrollTrigger work as on any tween.
import { Motly } from '@motly/gsap';
import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
// A ring that lasts 1 second on its own.
const ring = {
kind: 'circle',
radius: [8, 90],
fill: 'none',
stroke: 'deeppink',
strokeWidth: [10, 0],
duration: 1,
restAt: 0.3,
};
// Every key beside `spec` is GSAP's: squeeze it to 0.6 s, ease its time, repeat it forever.
gsap.effects.shape('.dot', {
spec: ring,
duration: 0.6,
ease: 'power2.out',
repeat: -1,
repeatDelay: 0.4,
});

keyframes, startAt, runBackwards and stagger do not apply to a burst: the types reject them, and if you pass them anyway they are ignored with a warning. To stagger bursts, use a loop or timeline positions.

An element, a selector or a list gives one burst from each element’s centre. The centre is read when the tween starts from 0 moving forward, not on every frame, so a burst stays where it began if the page scrolls.

A point, { x, y } in viewport pixels as clientX and clientY give, bursts from that point:

import { each, Motly, rand } from '@motly/gsap';
import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const spec = {
kind: 'burst',
count: 10,
radius: [0, 60],
restAt: 0.4,
children: {
kind: 'circle',
radius: [rand(3, 7), 0],
fill: each(['#ffd166', '#ef476f', '#06d6a0', '#118ab2']),
duration: rand(0.5, 0.8),
},
};
// A point in viewport pixels, as clientX and clientY give, instead of an element.
addEventListener('pointerdown', (event) => {
gsap.effects.burst({ x: event.clientX, y: event.clientY }, { spec });
});

Each element or point gets its own Seed, so a row of icons does not show the same burst copied. seed: 7 draws the same bursts on every page load: the element or point at index i draws from seed + i. Without seed, each call draws a random Seed, which a repeat, a restart and a scrub all keep.

import { each, Motly, rand } from '@motly/gsap';
import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const spec = {
kind: 'burst',
count: 9,
radius: [0, rand(40, 70)],
restAt: 0.4,
children: {
kind: 'star',
points: 5,
radius: [rand(4, 10), 0],
angle: [0, rand(-180, 180)],
fill: each(['gold', 'deeppink', 'white']),
duration: rand(0.5, 0.9),
},
};
// One burst per matching element; the ith draws from seed + i, so the three differ from each
// other but are the same on every click and every page load.
document.querySelector('#seeded').addEventListener('click', () => {
gsap.effects.burst('.icon', { spec, seed: 7 });
});
// Without a seed, every call draws a fresh random burst.
document.querySelector('#random').addEventListener('click', () => {
gsap.effects.burst('.icon', { spec });
});

A selector or list that matches nothing warns, and gives a tween as long as the Spec that draws nothing, so a timeline keeps its timing.

By default, a burst is painted in an overlay the tween owns: fixed over the viewport, above the page, catching no clicks, and in the document only while the tween is between its ends.

container, an element or a selector, paints it inside that element instead, so it scrolls with it and is clipped by it. A burst scrubbed by ScrollTrigger in a pinned section needs this.

import { Motly, rand } from '@motly/gsap';
import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const spec = {
kind: 'burst',
count: 16,
radius: [0, 200],
restAt: 0.3,
children: { kind: 'circle', radius: [rand(5, 10), 0], fill: 'deeppink', duration: 1 },
};
const button = document.querySelector('button');
button.addEventListener('click', () => {
// The burst still comes from the button, but it is painted in the card.
gsap.effects.burst(button, { spec, container: '.card' });
});

The container must have a size. A static container is made relative while a burst is drawn in it, and set back once the last one is cleared. A selector that matches nothing warns and gives a tween as long as the Spec that draws nothing.

renderer is 'auto' by default, which keeps bursts under 50 Elements in SVG and paints larger ones on a canvas. 'svg' or 'canvas' forces one.

A burst never outlives its tween, wherever GSAP reaches the tween:

  • At either end, nothing is left drawn.
  • tween.kill(), tween.revert(), tl.revert() and gsap.context().revert() clear a burst mid-flight. So does useGSAP when its component unmounts:
useGSAP(
() => {
gsap.effects.burst(buttonRef.current, { spec });
},
{ scope: containerRef },
);

Give onInterrupt in vars. motly chains its own clear-up before yours there. Replacing it later with tween.eventCallback('onInterrupt', fn) drops the clear-up, so a later kill() leaves the burst drawn. For the same reason, reading it back, through eventCallback() or tween.vars, gives motly’s wrapper rather than your function.

A viewer who prefers reduced motion sees the Spec’s still Resting frame, at spec.restAt, for the tween’s full length, so everything after it in a timeline keeps its timing. The preference is read each time the tween starts from 0, so changing it needs no reload; a repeat keeps the one it started with. reducedMotion: 'always' or 'never' in vars forces either, for a demo or a test:

import { Motly, rand } from '@motly/gsap';
import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const spec = {
kind: 'burst',
count: 12,
radius: [0, 80],
// The Resting frame: what a viewer who prefers reduced motion sees, for the whole tween.
restAt: 0.4,
children: { kind: 'circle', radius: [rand(4, 9), 0], fill: 'gold', duration: 1.2 },
};
// Left out, reducedMotion follows the viewer's setting. These force it, as a demo or test would.
for (const button of document.querySelectorAll('button')) {
button.addEventListener('click', () => {
gsap.effects.burst(button, { spec, reducedMotion: button.dataset.reducedMotion });
});
}

This covers the bursts only. Your own tweens around them keep moving; wrap those in gsap.matchMedia() with (prefers-reduced-motion: reduce).

Importing the package types gsap.effects.burst, gsap.effects.shape, tl.burst and tl.shape: spec must be a Spec of the right kind. BurstVars, ShapeVars, MotlyVars and RendererName are exported for vars built apart from the call:

import type { BurstVars } from '@motly/gsap';
const vars: BurstVars = { spec: { kind: 'burst', children: { kind: 'circle' } }, seed: 7 };
gsap.effects.burst('.icon', vars);