Examples
Each example below is complete: copy its JavaScript and its HTML with the buttons on the code, and
it runs with @motly/gsap and gsap installed. With script tags instead, drop the imports and use
Motly.rand and Motly.each; the script build registers itself.
The pens
Section titled “The pens”Five examples that each show something only a GSAP plugin can do. They are also on CodePen, loading everything from a CDN.
Heart like burst
Section titled “Heart like burst”Timeline sequencing: the heart squashes, then a ring, a pop and sparks, each placed with GSAP’s position parameter. Unliking mid-burst reverts the timeline, which clears it. Open on CodePen.
import { each, Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const ring = { kind: 'circle', radius: [10, 52], fill: 'none', stroke: ['#ff4d6d', '#ffb3c1'], strokeWidth: [12, 0], duration: 0.45, restAt: 0.3,};
const sparks = { kind: 'burst', count: 14, radius: [30, 80], restAt: 0.4, children: { kind: 'circle', radius: [rand(3, 6), 0], fill: each(['#ff4d6d', '#ffd166', '#8338ec', '#3a86ff']), duration: rand(0.5, 0.8), },};
const heart = document.querySelector('.heart');let like;
heart.addEventListener('click', () => { const liked = heart.classList.toggle('liked'); heart.setAttribute('aria-pressed', String(liked)); // revert() clears a burst mid-flight and puts the scale back; kill() would leave it drawn. like?.revert(); if (!liked) return; like = gsap .timeline() .to(heart, { scale: 0.6, duration: 0.12, ease: 'power2.in' }) .shape(heart, { spec: ring }) .to(heart, { scale: 1, duration: 0.6, ease: 'elastic.out(1.2, 0.4)' }, '<') .burst(heart, { spec: sparks }, '<0.05');});<main> <button type="button" class="heart" aria-label="Like" aria-pressed="false">♥</button> <p>Timeline sequencing: squash, then a ring, a pop and sparks.</p></main>
<style> body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #1b1025; font-family: system-ui, sans-serif; color: #f5e9ff; } .heart { font-size: 64px; line-height: 1; color: #6b5a78; background: none; border: 0; cursor: pointer; transition: color 0.2s; } .heart.liked { color: #ff4d6d; } main { text-align: center; } p { opacity: 0.6; }</style>Confetti cannon
Section titled “Confetti cannon”Three bursts in one timeline: from the button, then from both sides of the screen at once. Open on CodePen.
import { each, Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const palette = ['deeppink', 'gold', 'cyan', 'mediumpurple', 'limegreen'];const piece = { fill: each(palette), stroke: each(palette), duration: rand(0.9, 1.5) };
/** Confetti thrown `reach` pixels out: stars, squares, dots and streamers. */const confetti = (count, reach) => ({ kind: 'burst', count, radius: [0, reach], easing: 'ease-out', restAt: 0.35, children: each([ { kind: 'star', points: 5, radius: [rand(6, 12), 0], angle: [0, rand(-270, 270)], ...piece }, { kind: 'polygon', points: 4, radius: [rand(4, 9), 0], angle: [0, rand(-180, 180)], ...piece }, { kind: 'circle', radius: [rand(3, 7), 0], ...piece }, { kind: 'line', radius: [rand(5, 10), 0], angle: [0, rand(-360, 360)], ...piece }, ]),});
const button = document.querySelector('.celebrate');
button.addEventListener('click', () => { // Points are viewport pixels, read when the click happens. const left = { x: innerWidth * 0.15, y: innerHeight * 0.3 }; const right = { x: innerWidth * 0.85, y: innerHeight * 0.3 }; gsap .timeline() .to(button, { scale: 0.92, duration: 0.1, yoyo: true, repeat: 1 }) .burst(button, { spec: confetti(28, 140) }, '<') // 0.25 s after the button's burst starts, both sides at once. .burst(left, { spec: confetti(40, 220) }, '<0.25') .burst(right, { spec: confetti(40, 220) }, '<');});<main> <button type="button" class="celebrate">Celebrate</button> <p>tl.burst's position parameter: the button, then both sides together.</p></main>
<style> body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #fdf6e3; font-family: system-ui, sans-serif; } button { font: 600 20px system-ui, sans-serif; padding: 16px 32px; border: 0; border-radius: 999px; background: #3a0ca3; color: white; cursor: pointer; } p { text-align: center; opacity: 0.6; }</style>Scroll-scrubbed fireworks
Section titled “Scroll-scrubbed fireworks”ScrollTrigger scrubs a timeline of rockets and shells, back and forth. The shells are painted in the pinned section, so they scroll with it. Scroll inside the example. Open on CodePen.
import { each, Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(Motly, ScrollTrigger);
// A shell of 10 bursts of 24 sparks: 240 Elements, so 'auto' paints it on a canvas.const shell = (colors) => ({ kind: 'burst', count: 10, radius: [0, 60], restAt: 0.4, children: { kind: 'burst', count: 24, radius: [0, rand(40, 70)], children: { kind: 'circle', radius: [rand(1.5, 3.5), 0], fill: each(colors), duration: rand(0.9, 1.5), }, },});
const [first, second, third] = gsap.utils.toArray('.rocket');// The section is the container: bursts scroll with it and are clipped by it.const inSky = { container: '.sky' };
gsap .timeline({ scrollTrigger: { trigger: '.sky', start: 'top top', end: '+=2500', scrub: 1, pin: true }, }) // Each rocket rises, then its shell opens where it stopped as the rocket fades. .to(first, { y: '-65vh', duration: 1, ease: 'power2.out' }) .burst(first, { spec: shell(['gold', 'orangered']), ...inSky }) .to(first, { opacity: 0, duration: 0.2 }, '<') .to(second, { y: '-75vh', duration: 1, ease: 'power2.out' }, '<0.3') .burst(second, { spec: shell(['deepskyblue', 'white']), ...inSky }) .to(second, { opacity: 0, duration: 0.2 }, '<') .to(third, { y: '-55vh', duration: 1, ease: 'power2.out' }, '<0.2') .burst(third, { spec: shell(['deeppink', 'mediumpurple', 'gold']), ...inSky }) .to(third, { opacity: 0, duration: 0.2 }, '<');<div class="note">Scroll down: ScrollTrigger scrubs the show, back and forth.</div><section class="sky"> <div class="rocket" style="left: 25%"></div> <div class="rocket" style="left: 55%"></div> <div class="rocket" style="left: 80%"></div></section><div class="note">The bursts are painted inside the section, so they scroll with it.</div>
<style> body { display: block; place-items: normal; margin: 0; background: #05081a; color: #cdd6ff; font-family: system-ui, sans-serif; } .note { height: 70vh; display: grid; place-items: center; font-size: 20px; opacity: 0.7; } .sky { position: relative; height: 100vh; overflow: hidden; background: linear-gradient(#05081a, #1a1f4a); } .rocket { position: absolute; bottom: 0; width: 6px; height: 18px; margin-left: -3px; border-radius: 3px; background: linear-gradient(#fff6c2, #ff7b00); }</style>Click sparkles
Section titled “Click sparkles”Every click bursts from its own point. Open on CodePen.
import { each, Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const sparkle = { kind: 'burst', count: 9, radius: [0, rand(40, 70)], stagger: 0.02, restAt: 0.4, children: { kind: 'star', points: 4, innerRadius: 0.3, radius: [rand(6, 11), 0], angle: [0, rand(-90, 90)], fill: each(['#ffd60a', '#ffffff', '#90e0ef']), duration: rand(0.4, 0.7), },};
// A point is { x, y } in viewport pixels, exactly what clientX and clientY give.addEventListener('pointerdown', (event) => { gsap.effects.burst({ x: event.clientX, y: event.clientY }, { spec: sparkle });});<p>Click anywhere: each click bursts from its point.</p>
<style> body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #0d1b2a; color: #e0e1dd; font-family: system-ui, sans-serif; user-select: none; cursor: crosshair; }</style>Reversible ripple toggle
Section titled “Reversible ripple toggle”Clicking again mid-ripple reverses the timeline, and the ripple runs back into the button. Open on CodePen.
import { each, Motly } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const ripple = { kind: 'burst', count: 16, radius: [0, 110], stagger: { each: 0.04, easing: 'ease-in' }, restAt: 0.5, children: { kind: 'star', points: 4, innerRadius: 0.35, radius: [14, 0], angle: [0, 90], fill: each(['#ef6f6c', '#f7b267']), duration: 0.7, },};
const toggle = document.querySelector('.toggle');const tl = gsap .timeline({ paused: true }) .to('.toggle span', { rotate: 135, duration: 0.5, ease: 'back.out(2)' }) .burst(toggle, { spec: ripple }, 0);
toggle.addEventListener('click', () => { const opening = tl.progress() === 0 || tl.reversed(); opening ? tl.play() : tl.reverse(); toggle.setAttribute('aria-expanded', String(opening)); toggle.setAttribute('aria-label', opening ? 'Close' : 'Open');});<main> <button type="button" class="toggle" aria-label="Open" aria-expanded="false"><span>+</span></button> <p>Reversing: click again mid-ripple and it runs back into the button.</p></main>
<style> body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f1f4f9; font-family: system-ui, sans-serif; } .toggle { width: 72px; height: 72px; border: 0; border-radius: 50%; background: #0f4c81; color: white; font-size: 40px; line-height: 1; cursor: pointer; } .toggle span { display: inline-block; } main { text-align: center; } p { opacity: 0.6; }</style>Small ideas
Section titled “Small ideas”Sparkle on hover
Section titled “Sparkle on hover”A burst around a card as the pointer enters it. Reverting the last one first keeps quick hovers from piling up.
import { each, Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const sparkle = { kind: 'burst', count: 8, radius: [40, rand(90, 120)], restAt: 0.3, children: { kind: 'star', points: 4, innerRadius: 0.3, radius: [rand(4, 8), 0], fill: each(['#ffd60a', '#ffffff']), duration: rand(0.5, 0.8), },};
const card = document.querySelector('.card');let hover;
function sparkleCard() { // One sparkle at a time: a new hover replaces the last one instead of piling up. hover?.revert(); hover = gsap.effects.burst(card, { spec: sparkle });}
card.addEventListener('pointerenter', sparkleCard);card.addEventListener('focus', sparkleCard);<button type="button" class="card"> <strong>Pro plan</strong> <span>Hover or focus me</span></button>
<style> .card { display: grid; gap: 0.25rem; padding: 1.5rem 2.5rem; border: 1px solid #ffffff33; border-radius: 1rem; background: #2d1b3d; color: inherit; font: inherit; text-align: center; cursor: pointer; } span { opacity: 0.6; }</style>Copied
Section titled “Copied”A ring and a few dots confirm a click, and the label changes back once the timeline reaches 1.5 seconds.
import { Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const ring = { kind: 'circle', radius: [30, 70], fill: 'none', stroke: '#06d6a0', strokeWidth: [6, 0], duration: 0.5, restAt: 0.2,};const dots = { kind: 'burst', count: 6, radius: [40, 80], restAt: 0.3, children: { kind: 'circle', radius: [rand(2, 4), 0], fill: '#06d6a0', duration: 0.5 },};
const button = document.querySelector('button');
button.addEventListener('click', () => { button.textContent = 'Copied'; gsap .timeline() .shape(button, { spec: ring }) .burst(button, { spec: dots }, '<0.05') .call( () => { button.textContent = 'Copy link'; }, [], 1.5, );});<button type="button">Copy link</button>
<style> button { min-width: 9rem; padding: 0.75rem 1.25rem; border: 1px solid #ffffff44; border-radius: 0.5rem; background: #2d1b3d; color: inherit; font: 600 1rem system-ui, sans-serif; cursor: pointer; }</style>Unread ping
Section titled “Unread ping”A ring that repeats until the notification is read. revert() stops it and clears it mid-flight.
import { Motly } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const ping = { kind: 'circle', radius: [20, 60], fill: 'none', stroke: '#ef476f', strokeWidth: [6, 0], duration: 1, restAt: 0.2,};
// An unread notification pings until it is read.const pinging = gsap.effects.shape('.bell', { spec: ping, repeat: -1, repeatDelay: 0.6 });
document.querySelector('button').addEventListener('click', (event) => { // revert() clears the ring mid-flight; the tween leaves nothing drawn. pinging.revert(); event.currentTarget.disabled = true; event.currentTarget.textContent = 'All read';});<main> <span class="bell" aria-hidden="true">🔔</span> <button type="button">Mark as read</button></main>
<style> main { display: grid; justify-items: center; gap: 2.5rem; } .bell { font-size: 2.5rem; } button { padding: 0.5rem 1rem; border: 1px solid #ffffff44; border-radius: 999px; background: none; color: inherit; font: 0.9rem system-ui, sans-serif; cursor: pointer; }</style>Jelly pop
Section titled “Jelly pop”A path Curve copied from a design tool: it overshoots to 120%, falls back to 90% and settles.
import { Motly } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
// An SVG path curve, as drawn in a design tool: overshoot to 120%, fall back to 90%, settle.const jelly = 'M0,100 C15,40 25,-20 40,-20 C55,-20 60,10 70,10 C80,10 90,0 100,0';
const spec = { kind: 'polygon', points: 6, radius: [0, 70], angle: [-30, 0], fill: '#8338ec', easing: { radius: jelly, default: 'ease-out' }, duration: 1.2,};
gsap.effects.shape('.origin', { spec, repeat: -1, repeatDelay: 0.8 });<div class="origin"></div>Swirling confetti
Section titled “Swirling confetti”Mixed pieces, each wrapped in a Swirl so it wriggles outward, with neighbours curling opposite ways.
import { each, Motly, rand } from '@motly/gsap';import { gsap } from 'gsap';
gsap.registerPlugin(Motly);
const palette = ['#ffd166', '#ef476f', '#06d6a0', '#118ab2'];const piece = { fill: each(palette), angle: [0, rand(-360, 360)], duration: rand(0.9, 1.4) };
// Each piece is wrapped in a Swirl, so it wriggles outward; neighbours curl opposite ways.const swirl = (child) => ({ kind: 'swirl', size: rand(20, 40), frequency: rand(1, 2), direction: each([1, -1]), child,});
const spec = { kind: 'burst', count: 24, radius: [0, rand(120, 160)], easing: 'ease-out', restAt: 0.35, // A Swirl's child is one Spec, so each() hands out whole Swirls, one per kind of piece. children: each([ swirl({ kind: 'polygon', points: 4, radius: [rand(4, 7), 0], ...piece }), swirl({ kind: 'star', points: 5, radius: [rand(5, 9), 0], ...piece }), swirl({ kind: 'circle', radius: [rand(3, 5), 0], ...piece }), ]),};
const button = document.querySelector('button');button.addEventListener('click', () => gsap.effects.burst(button, { spec }));<button type="button">Launch</button>
<style> button { padding: 0.75rem 1.5rem; border: 0; border-radius: 999px; background: #8338ec; color: white; font: 600 1rem system-ui, sans-serif; cursor: pointer; }</style>