There's a difference between a $20k website and a $200k website. It's not the copy. It's not the design. It's not the number of illustrations. It's the feel — the moment you scroll down a section and it pins to the viewport, animation kicking off frame-by-frame with your scroll wheel. The browser isn't fetching new content. The page isn't loading. It's just smooth, intentional motion tied directly to your input. That's GSAP ScrollTrigger. That's the difference between feeling like you've hired an agency and feeling like you've hired someone who actually knows what they're doing.
GSAP (GreenSock Animation Platform) is animation-first JavaScript. It's been the industry standard for 15+ years. ScrollTrigger is the plugin that ties animations to scroll position. Pin a section to the viewport and animate its contents as the user scrolls past. The catch: pinning breaks the DOM flow. You add content that doesn't exist in the layout tree. That's where pinspacer comes in — a hidden element that holds space for your pinned content while the animation plays. This is the detail that breaks most implementations. Get it wrong and your next section overlaps, your scroll feels janky, users think your site is broken. Get it right and you get Apple 2024 site.
What Scroll-Pinning Actually Unlocks
A normal website scrolls top-to-bottom. Content flows. You read. You scroll. It's passive. Scroll-pinning is different: you keep a section visible while the scroll position advances. On first read, this sounds impossible — if the section is pinned to the viewport, how does scroll move past it? The answer: the browser scrolls but the element doesn't move. Meanwhile, content inside that element animates frame-by-frame with your scroll velocity. The section feels sticky. But it's not sticky — the DOM scroll is advancing, the pixels just aren't moving until you've scrolled past the entire pinned region.
This is why Apple's 2024 redesign feels so good. Scroll down a feature section. A heading pins. As you continue scrolling, the heading fades out while the next piece of copy fades in. Everything's happening inside a pinned container. To your eye, it's pure cinema. To the DOM, it's a calculated position transform and opacity change. The motion is tied to your scroll input, not animated on a fixed timeline. That's what makes it feel intentional and expensive.
Velocity X (Aidxn's process-driven marketing template) uses this extensively in the ProcessSticky component. The 4-step onboarding section is pinned. As you scroll, each step slides in, the background SVG morphs, the progress marker advances. By the time you've scrolled 80vh past the section start, you're looking at step 4. Scroll back up and it animates in reverse. No JavaScript state. No animation framework re-running. Just pure scroll-driven DOM mutation. This is what separates template sites from bespoke work.
Understanding Pinspacer (The Part That Breaks Everything)
Here's the problem: When you pin an element, you're telling the browser "keep this element fixed to the viewport." But the element still takes up space in the DOM. If you're pinning a section with `height: 100vh`, the browser is saying "this element is fixed, so skip it in the flow." Result: the content below the pinned section jumps up and overlaps it.
To fix this, you need a spacer — an invisible placeholder that holds the space where the pinned element would be if it was still in the DOM flow. That's pinspacer. ScrollTrigger automatically creates it. Here's how:
gsap.registerPlugin(ScrollTrigger);
gsap.to('.hero-section', {
scrollTrigger: {
trigger: '.hero-section',
pin: true, // This element is pinned
// pinspacer is automatically created
duration: 100,
scrub: true
},
opacity: 0.5
});
When you set `pin: true`, ScrollTrigger watches the scroll position. As you approach the trigger element, it pins the element to the viewport AND creates an invisible div (the spacer) that holds space equal to the pinned element's height. When you scroll past the entire pinned region, the spacer disappears and the pinned element returns to the document flow.
The result: no overlapping. No jumpy layout. Just smooth, predictable space management. This is why pinspacer confuses people — it's invisible by default. You don't add it to your HTML. ScrollTrigger creates it programmatically. If your pinned section is 400vh tall (meaning it takes 4 viewports worth of scroll to animate through it), the spacer is 400vh tall, holding exactly that much space.
Three Patterns That Define Premium Sites
Pattern 1: Sticky Reveal (The Apple Pattern)
A heading or hero element pins to the viewport while content animates around it. Use case: feature announcements, headline transitions, or story progression where the focal point stays fixed while the supporting information cycles.
// HTML structure
<section class="features">
<div class="sticky-reveal">
<h2 class="headline">One thing. Perfectly done.</h2>
<div class="content-carousel">
<div class="slide">...</div>
<div class="slide">...</div>
<div class="slide">...</div>
</div>
</div>
</section>
// GSAP
const headline = document.querySelector('.headline');
const slides = document.querySelectorAll('.slide');
gsap.to('.sticky-reveal', {
scrollTrigger: {
trigger: '.sticky-reveal',
pin: true,
start: 'top top',
end: 'bottom center',
scrub: 1,
markers: true // remove in production
}
});
gsap.to(slides, {
y: 100,
opacity: 0,
stagger: 0.5,
scrollTrigger: {
trigger: '.sticky-reveal',
scrub: 1
}
});
As you scroll, the section pins. The headline stays centered. Each slide below fades and slides down in sequence. By the time you reach the bottom, all slides have exited. The next section enters normally. This is the core pattern of Velocity X's ProcessSticky and Apple's entire 2024 homepage.
Pattern 2: Horizontal Scroll-Jacking
Scroll vertically. Content moves horizontally. The browser's scroll input is intercepted and fed into a horizontal animation. Use case: timeline views, product carousels, case study galleries where you want users to feel like they're "pulling" content across the screen.
// HTML
<section class="scroll-hijack">
<div class="h-scroll-container">
<div class="h-item">Item 1</div>
<div class="h-item">Item 2</div>
<div class="h-item">Item 3</div>
</div>
</section>
// GSAP
gsap.to('.h-scroll-container', {
scrollTrigger: {
trigger: '.scroll-hijack',
pin: true,
scrub: 1,
markers: true
},
x: -1200, // move left by 1200px as user scrolls
duration: 3
});
Set `pin: true` on the section. Animate the x position based on scroll. As the user scrolls down, the container slides left. The pinspacer holds exactly enough vertical space for the full horizontal animation to complete. By the time you've scrolled past, you're looking at the last item. Scroll back up and it animates in reverse. The feel: you're pulling content across the screen with your scroll wheel. It's tactile. It's expensive.
Pattern 3: Scrubbing an Animation Timeline
Create a complex animation sequence (multiple elements, multiple properties, 3+ seconds long) and tie it directly to scroll position. A keyframe on frame 1 happens at scroll 0. A keyframe on frame 120 happens 120 scrolls later. The animation scrubs forward and backward with your scroll velocity. Use case: parallax hero sections, interactive infographics, or any animation where you want granular user control via scroll.
// Create a timeline
const tl = gsap.timeline();
tl.to('.element-1', { y: -100, opacity: 0, duration: 1 })
.to('.element-2', { x: 50, duration: 1 }, 0.5)
.to('.element-3', { rotation: 360, duration: 2 }, 0);
// Tie the entire timeline to scroll
ScrollTrigger.create({
trigger: '.animation-section',
start: 'top top',
end: 'bottom center',
scrub: 1, // smooth scrub
animation: tl,
markers: true
});
The `scrub` property (value between 0–1 where 1 = smooth) maps scroll position to timeline progress. Scroll fast, animation runs fast. Scroll slow, animation slows. This is how Stripe animates their pricing table and how luxury car sites let users "spin" a 3D model by scrolling.
Real Code: Velocity X ProcessSticky
This is simplified but reflects the real pattern used in Velocity 9 templates:
// ProcessSticky.tsx
import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export const ProcessSticky = () => {
const containerRef = useRef(null);
const stepsRef = useRef([]);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// Pin the container
const pin = gsap.to(container, {
scrollTrigger: {
trigger: container,
pin: true,
start: 'top top',
end: 'bottom center',
scrub: 0.5,
snap: 1 / stepsRef.current.length // snap to each step
}
});
// Animate steps in sequence
stepsRef.current.forEach((step, idx) => {
gsap.fromTo(
step,
{ opacity: 0, y: 50 },
{
opacity: 1,
y: 0,
duration: 0.5,
scrollTrigger: {
trigger: container,
containerAnimation: pin,
scrub: 0.5
},
stagger: 0.2
}
);
});
return () => {
pin.kill();
};
}, []);
return (
<section ref={containerRef} className="process-sticky">
<h2>The Process</h2>
{steps.map((step, i) => (
<div key={i} ref={(el) => stepsRef.current[i] = el}>
{step.title}
</div>
))}
</section>
);
};
Pin the container. Animate the steps inside. Lock the animation to the scroll trigger using `containerAnimation`. Snap between steps so users land cleanly on each one. The result: a 4-step journey that feels like the user is pulling content into view. This is what converts visitors into customers.
Six FAQs
Doesn't pinspacer add DOM bloat?
No. The spacer is a single div. It's invisible. It holds no content. The performance cost is negligible. The benefit (preventing overlapping layout) is worth 0.1kb of DOM.
Can I pin multiple sections on the same page?
Yes. Each pin gets its own pinspacer. Scroll down and hit a new pinned section — it pins while the previous one unpins. They don't interfere. Just manage your trigger positions carefully so pinned sections don't overlap.
What's the difference between `pin: true` and `position: sticky`?
`position: sticky` is CSS-only and respects the parent container. `pin: true` is JavaScript-driven and pins to the viewport regardless of parent. ScrollTrigger pin is more powerful for complex animations. Use `sticky` for simple cases (navbars), ScrollTrigger for cinema.
Does `scrub` hurt performance?
Not if you use `scrub: 0.5` to 1. GSAP is optimized for scroll-driven animations. It throttles updates intelligently. On mobile, use `scrub: 1` (smooth) instead of `scrub: 0` (every frame) to reduce jank.
Can I use ScrollTrigger with Astro?
Yes. Load GSAP in a client component or script. Initialize ScrollTrigger in an effect hook or `transition:persist` script. It works anywhere JavaScript runs. Velocity X uses this pattern extensively.
What's the conversion impact of scroll-driven animations?
Hard to quantify, but directionally: sites with intentional scroll motion outconvert static sites by 15–30% (depending on industry and audience). The feel matters. Users subconsciously associate smooth motion with quality and care. Premium sites earn premium prices.
The Bottom Line
Scroll-pinning isn't a gimmick. It's the signature pattern of every high-end marketing site built in the last 5 years. Apple, Stripe, Webflow, Framer — they all use it. And they all use GSAP ScrollTrigger because it's reliable, performant, and gives you pinspacer management for free.
The tricky part isn't the code. It's understanding that pinning breaks the DOM flow and spacers fix it. Once that clicks, the rest is just tweening coordinates and opacity. Want to feel the difference between a template site and bespoke work? Scroll through a Velocity X template, then scroll through your current marketing site. Notice how sticky the experience feels. That's pinspacer + scrubbing. That's what $200k buys.
For deeper patterns and full ProcessSticky implementations, check out how Astro View Transitions pairs with scroll animations for even smoother page transitions. Motion is the medium. GSAP is the tool.