GSAP Animation Generator

Writes a GSAP tween, with ScrollTrigger if you want it, and a reduced-motion guard that makes the movement optional rather than the arrival.

Live output

Enable JavaScript to customise; default output below.

Becomes the comment at the top. Worth writing: in six months it is the only thing that says what the numbers were for.

Any CSS selector. GSAP animates every match, which is what makes stagger work.

Method

from animates out of the values you give, into wherever CSS already put the element. to animates into them. fromTo states both ends and is the one that survives a re-run.

Live preview animation.js
/**
 * Fade the cards up as they enter
 *
 * Built with GSAP and ScrollTrigger. Install with npm i gsap,
 * or load it from a CDN and drop the import lines.
 */
import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';

gsap.registerPlugin( ScrollTrigger );

/*
 * Someone who has asked their system to reduce motion has asked for a reason:
 * large movement is genuinely unpleasant with a vestibular disorder. The elements
 * still end up where they belong; they arrive without the journey.
 */
const reduced = window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
const duration = reduced ? 0 : 0.6;

gsap.from( '.card', {
	y: 24,
	autoAlpha: 0,
	duration,
	delay: 0,
	ease: 'power2.out',
	stagger: 0.08,
	scrollTrigger: {
		trigger: '.cards',
		start: 'top 80%',
		end: 'bottom 20%',
		toggleActions: 'play none none none',
	},
} );

Output is valid and updates as you type.

GSAP’s API is small enough to learn in an afternoon and large enough that everybody writes the same twenty lines from memory, slightly wrong, every time. This writes them: a tween, a ScrollTrigger if you want one, and the reduced-motion guard that most generated animation code leaves out.

That guard is the opinion this tool has. Someone who has turned on “reduce motion” in their operating system has usually done it because large movement makes them ill. The generated code sets the duration to zero for them, so elements arrive in the right place without the journey, rather than disabling the animation and leaving things invisible.

How to use

  1. Name the animation. It becomes the comment, and in six months it is the only thing that says what the numbers were for.
  2. Set the target selector. GSAP animates every match, which is what makes stagger work.
  3. Choose the method: from for an entrance, to for a departure or a state change, fromTo when you want both ends written down.
  4. Pick what moves, how long it takes and what ease.
  5. Turn on ScrollTrigger if the animation should wait for the element to come into view.

Example

Cards fading up as they enter, the defaults:

import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';

gsap.registerPlugin( ScrollTrigger );

const reduced = window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
const duration = reduced ? 0 : 0.6;

gsap.from( '.card', {
	y: 24,
	autoAlpha: 0,
	duration,
	delay: 0,
	ease: 'power2.out',
	stagger: 0.08,
	scrollTrigger: {
		trigger: '.cards',
		start: 'top 80%',
		end: 'bottom 20%',
		toggleActions: 'play none none none',
	},
} );

Twenty-four pixels and six tenths of a second. Most animations that feel cheap are three times both.

Pitfalls

from leaves a flash if the CSS does not match. gsap.from() reads the element’s current state as the destination and jumps it to the start, which happens after first paint: for a moment the card is visible in its final position. Either set the starting state in CSS as well, or use fromTo, which states both ends and does not care what the stylesheet says.

The trigger is usually not the thing being animated. Point ScrollTrigger at the container and animate the children. Pointing it at the children gives you one trigger per element, all firing at their own scroll positions, which is occasionally what you want and usually a surprise.

start: 'top 80%' reads trigger-first, viewport-second. It means “when the top of the trigger reaches 80 percent of the way down the viewport”. Getting the order backwards is the commonest ScrollTrigger mistake, and turning on markers for a minute settles it faster than reading the documentation.

Scrub is not a speed, it is a lag. scrub: 0.6 means the tween takes six tenths of a second to catch up with the scroll position. That lag is what reads as weight. scrub: true follows the scrollbar exactly and feels mechanical.

Pinning breaks on phones more often than anything else. It changes the document height and fights with mobile browsers’ collapsing address bars. Test it on a real handset before shipping, and consider turning it off under a breakpoint.

autoAlpha, not opacity. autoAlpha sets visibility: hidden at zero, so a faded-out element stops catching clicks. An invisible element at opacity: 0 still swallows them, which is how you get a page with an unclickable button nobody can explain.

Stagger past about 0.1 seconds outstays its welcome. With twelve cards at 0.15, the last one arrives nearly two seconds after the first, by which time the reader has moved on.

repeat: -1 belongs in loading states only. An indefinite animation in the body of a page is a permanent distraction, and it keeps the compositor awake and the battery draining.

ScrollTrigger needs to be registered once, not per file. The generated code registers it because it has to work standing alone; if you paste several of these into one bundle, one registerPlugin call is enough.

Compatibility

GSAP 3 and its ScrollTrigger plugin, which work in every browser that supports ES2015. The generated file is an ES module using import gsap from 'gsap': if you load GSAP from a CDN as a global instead, delete the import lines and everything else works unchanged.

The code is generated by a template with per-context escaping, so a selector containing a quote comes out escaped rather than closing the string. The test fixtures include a hostile case with '); alert(1) in the selector, </script> in the trigger and */ in the name, and the suite checks that what comes out still parses as JavaScript.

Scale is a fixed list rather than a free number, because it is the one value that goes straight into the generated code as a bare literal, and a list is a stronger guarantee than an escape.

prefers-reduced-motion is supported in every current browser and has been since 2019. The generated code reads it once at module scope rather than on every tween, which is enough for a page and slightly wrong for a long-lived application: someone can change the setting without reloading, and a matchMedia().addEventListener( 'change', … ) is the thorough version.

Frequently asked questions

Do I need the paid GSAP plugins?
Not for this. ScrollTrigger became free in GSAP 3.11 along with the rest of the former Club plugins; what is generated here uses only core and ScrollTrigger.
Should I use GSAP or CSS animations?
CSS for hovers, state changes and anything a transition can express. GSAP once you need sequencing, scroll positions, physics-flavoured easing or fine control over timelines. A site that uses GSAP for a button hover is carrying a library it does not need.
Why is my animation not firing?
Nine times in ten the trigger element is not where you think. Turn on markers, reload, and look at where the start line lands. Ten times in ten after that it is because the element was added to the page after ScrollTrigger measured, which ScrollTrigger.refresh() fixes.
How do I chain several of these?
Put them in a timeline: const tl = gsap.timeline(); then tl.from(…).from(…, '-=0.3'). This tool writes single tweens, which is the right unit to copy into a timeline.
Does this hurt Core Web Vitals?
Animating transform and opacity does not cause layout, so it is cheap. Animating width, height, top or margin is not, and an entrance animation that moves content around after load is a direct contribution to Cumulative Layout Shift.
Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.