Pin
The section stays put while the page keeps scrolling underneath.
Eight effects from real projects. With code.
Contents// Hover any row in the index above
gsap.to(title, {
duration: 0.7,
overwrite: true,
scrambleText: {
text: title.dataset.text,
chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZ#%&*",
revealDelay: 0.1,
},
});SplitText + ScrollTrigger
Every line gets its own mask. The scrollbar does the printing.
SplitText checks where the browser actually broke your lines, wraps each one in a mask and splits again when fonts load or the window changes size. Every headline on this site runs on it. The paragraph on the right is tied to the scroll, so scroll back up and watch it unprint.
Good motion works like good typesetting. It has rhythm, it knows where the line breaks, and it never makes you wait for the point.
SplitText.create(paragraph, {
type: "lines,words",
mask: "lines", // each line clips itself
autoSplit: true, // re-split on resize & font load
onSplit(self) {
return gsap.timeline({
scrollTrigger: { trigger: paragraph, start: "top 80%", end: "bottom 35%", scrub: 1 },
})
.from(self.lines, { yPercent: 100, stagger: 0.1 })
.fromTo(self.words, { opacity: 0.12 }, { opacity: 1, stagger: 0.02 }, 0);
},
});Flip
Swap a class, keep the thread. Flip remembers where everything was.
You never animate to a layout. You change the class, and Flip works out how each tile gets from its old spot to its new one. Grid, list and stack are three plain CSS rules. The motion comes free.
// 1. record where everything is
state = Flip.getState(".flip__tile");
// 2. let Vue change the class
layout.value = "stack";
// 3. after the DOM updates, animate from old → new
await nextTick();
Flip.from(state, {
duration: 0.9,
ease: "power3.inOut",
stagger: 0.04,
absolute: true,
});Draggable + InertiaPlugin
Physical UI is mostly about letting go.
Draggable handles the pointer, touch and the edges. InertiaPlugin reads how fast you let go and lets each sticker glide out, bouncing off the sides. A little tilt on the way and a little wobble on landing, and suddenly it feels like paper.
Grab and throw
Draggable.create(".sticker", {
bounds: desk,
inertia: true, // InertiaPlugin: momentum on release
edgeResistance: 0.7,
onDrag() {
// tilt into the direction of travel
gsap.to(this.target, { rotation: this.deltaX * 1.6 });
},
onThrowComplete() {
gsap.to(this.target, { rotation: gsap.utils.random(-12, 12), ease: "elastic.out(1, 0.5)" });
},
});Raw WebGL
No three.js, no textures. One fragment shader and three inks.
Noise folded into itself makes those slow, inky waves. I squash it down to paper, black and red, then sprinkle grain on top that changes every frame, so it flickers like a real riso pass. Your cursor pushes the ink around. If you like it, the export page turns it into a poster.
Make a poster// fragment shader: domain-warped fbm
vec2 q = vec2(fbm(uv * 1.6 + t), fbm(uv * 1.6 + vec2(5.2, 1.3) - t));
float pull = exp(-length(uv - mouse) * 3.5);
vec2 r = vec2(fbm(uv * 1.6 + 4.0 * q + pull), fbm(uv * 1.6 + 4.0 * q - pull));
float f = fbm(uv * 1.6 + 4.0 * r) + grain * 0.07;
// posterize into three spot inks
vec3 col = f < 0.37 ? ink : f < 0.47 ? accent : paper;ScrollTrigger · containerAnimation
You scroll down. The story moves left.
The section below pins itself and slides four panels across while you keep scrolling. Each giant word has its own trigger, measured against the sideways movement instead of the page, so it drifts and leans as it passes.
const slide = gsap.to(track, {
x: () => -(track.scrollWidth - innerWidth),
ease: "none",
scrollTrigger: { trigger: section, pin: true, scrub: 1, invalidateOnRefresh: true },
});
// nested triggers ride the horizontal tween
gsap.fromTo(word, { xPercent: 20 }, {
xPercent: -20,
scrollTrigger: { trigger: word, containerAnimation: slide, scrub: true },
});The section stays put while the page keeps scrolling underneath.
The timeline's playhead hangs straight off the scrollbar.
Child triggers measure against the sideways tween, not the page.
Unpin, hand the scroll back, on to the next one.
gsap.ticker · utils.wrap
A marquee with a speedometer.
One ticker callback chases your scroll speed, wraps each row at half its width and leans the type the faster you go. Stop, and it settles back into a lazy drift. The band under the homepage cover is this exact component.
gsap.ticker.add((_t, delta) => {
velocity += (scrollY - lastY - velocity) * 0.12;
lastY = scrollY;
offset += dir * (0.6 + Math.abs(velocity) * 0.5) * (delta / 16.67);
offset = gsap.utils.wrap(-row.scrollWidth / 2, 0, offset);
row.style.translate = `${offset}px 0`;
setSkew(gsap.utils.clamp(-14, 14, -velocity * 0.4));
});Variable fonts · CSS custom properties
Bricolage Grotesque has a weight axis and a width axis. Distance drives both.
Every letter carries two custom properties that end up in font-variation-settings. GSAP eases them letter by letter depending on how far away your pointer is, so the type turns thin and narrow wherever you look. The big name on the homepage is the same component.
Come closer.
const weight = gsap.utils.pipe(
gsap.utils.clamp(0, radius),
gsap.utils.mapRange(0, radius, 200, 800),
);
// .px-char { font-variation-settings: "wght" var(--wght), "wdth" var(--wdth) }
for (const c of chars) {
const d = distance(pointer, center(c));
gsap.to(c, { "--wght": weight(d), "--wdth": width(d), overwrite: "auto" });
}Canvas 2D · lenticular
The prize from the cereal box, rebuilt.
Both pictures are painted offscreen, then redrawn every frame as interleaved seven-pixel strips. Each strip picks its side from the viewing angle plus its own position, so the image flips in a sweep instead of all at once. A ridge pattern and a travelling sheen do the rest.
Move across the print
for (let x = 0; x < w; x += strip) {
// each lens flips a little later than its neighbour → a sweep
const local = angle + (x / w - 0.5) * 0.6;
const t = clamp(0, 1, (local + 0.15) / 0.3);
ctx.globalAlpha = 1;
ctx.drawImage(sideA, x, 0, strip, h, x, 0, strip, h);
ctx.globalAlpha = smoothstep(t);
ctx.drawImage(sideB, x, 0, strip, h, x, 0, strip, h);
}
// ridges: a strip-wide gradient repeated as a pattern
ctx.fillStyle = ctx.createPattern(lens, "repeat");
ctx.fillRect(0, 0, w, h);Library
Further reading
Bricolage Grotesque, Instrument Serif, JetBrains Mono
GSAP 3: SplitText, ScrambleText, Flip, Draggable, Inertia, ScrollTrigger · Canvas 2D · WebGL
Lenis, synced to the GSAP ticker
Nuxt 3 · Vue 3 · Tailwind