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.
/**
* 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.
Fix the highlighted fields to update the output.
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
- Name the animation. It becomes the comment, and in six months it is the only thing that says what the numbers were for.
- Set the target selector. GSAP animates every match, which is what makes stagger work.
- Choose the method:
fromfor an entrance,tofor a departure or a state change,fromTowhen you want both ends written down. - Pick what moves, how long it takes and what ease.
- 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?
Should I use GSAP or CSS animations?
Why is my animation not firing?
ScrollTrigger.refresh() fixes.How do I chain several of these?
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.