GSAP
Adds scroll choreography — pinning, scrubbing, horizontal scroll, SplitText. Loads when the design needs runtime control on top of CSS effects.
GSAP Add GSAP when the design calls for scroll pinning, scroll-scrubbed animations, horizontal scroll panels, or text splitting. Everything from the Lightweight tier still applies — GSAP adds runtime control on top of CSS effects.
Files to load
<!-- 1–3. All Lightweight files -->
<link rel="stylesheet" href="tokens/animations.css">
<link rel="stylesheet" href="tokens/motion-classes.css">
<link rel="stylesheet" href="css/animations.css">
<!-- 4. GSAP core + ScrollTrigger via CDN -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/ScrollTrigger.min.js"></script>
<!-- Optional: SplitText for text splitting -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/SplitText.min.js"></script>GSAP loads from CDN and is typically cached across sites. The plugin files are ~20–40 KB each. Register ScrollTrigger before use:
gsap.registerPlugin(ScrollTrigger);
// If using SplitText:
gsap.registerPlugin(ScrollTrigger, SplitText);Pinned scroll sections
A pinned section stays fixed in the viewport while the page scrolls through it. Content inside animates in sync with scroll progress.
gsap.to('.pinned-content', {
x: '-200%',
ease: 'none',
scrollTrigger: {
trigger: '.pinned-section',
pin: true,
scrub: 1,
end: '+=300%',
},
});<section class="pinned-section">
<div class="pinned-content">
<div class="panel">Step 1</div>
<div class="panel">Step 2</div>
<div class="panel">Step 3</div>
</div>
</section>Scroll-scrubbed animations
Scrub ties an animation's playhead directly to scroll position. The animation does not play on its own — it is driven frame-by-frame by the user's scroll.
gsap.from('.hero-image', {
scale: 1.2,
opacity: 0,
ease: 'none',
scrollTrigger: {
trigger: '.hero',
start: 'top top',
end: 'bottom center',
scrub: true,
},
});scrub: true— locks to scroll position (no lag).scrub: 1— adds 1-second smoothing (recommended for image / scale effects).
Horizontal scroll
A horizontal scroll panel scrolls its children horizontally while the user scrolls vertically. Works by pinning the container and translating its children.
const panels = gsap.utils.toArray('.h-panel');
gsap.to(panels, {
xPercent: -100 * (panels.length - 1),
ease: 'none',
scrollTrigger: {
trigger: '.h-scroll-container',
pin: true,
scrub: 1,
snap: 1 / (panels.length - 1),
end: () => '+=' + document.querySelector('.h-scroll-container').offsetWidth,
},
});<div class="h-scroll-container">
<div class="h-panels-track">
<div class="h-panel">Services</div>
<div class="h-panel">Portfolio</div>
<div class="h-panel">Team</div>
</div>
</div>Text splitting
SplitText breaks heading text into individual characters, words, or lines for animated reveal effects. Letters fly in, words fade up staggered, lines rise from behind a clip mask.
// Split a heading into characters
const split = new SplitText('.hero-headline', { type: 'chars,words' });
gsap.from(split.chars, {
opacity: 0,
y: 40,
stagger: 0.03,
duration: 0.6,
ease: 'power3.out',
scrollTrigger: {
trigger: '.hero-headline',
start: 'top 80%',
},
});Accessibility: SplitText wraps each character in a span. Screen readers still read the original text because GSAP preserves the parent element's text content. Add aria-label on the parent if the visual split creates ambiguity.
Stagger with GSAP
CSS stagger helpers (.bds-stagger-1 through 6) work for static item counts. Use GSAP when:
- Item count is dynamic (from a data source)
- You need the stagger to reverse on exit
- You need
from()semantics (items start below, stagger up in order) - You need a non-linear stagger (grid, random, ease-applied stagger)
// Dynamic stagger on all matching elements
gsap.from('.card', {
opacity: 0,
y: 24,
stagger: 0.08,
duration: 0.5,
ease: 'power2.out',
scrollTrigger: {
trigger: '.card-grid',
start: 'top 75%',
},
});Batch reveals
ScrollTrigger.batch reveals multiple elements as they enter the viewport — more performant than one ScrollTrigger per element for large lists.
ScrollTrigger.batch('.reveal-item', {
onEnter: (elements) =>
gsap.to(elements, {
opacity: 1,
y: 0,
stagger: 0.1,
duration: 0.6,
ease: 'power2.out',
}),
start: 'top 85%',
});