Interactive Storytelling: Scroll-Linked Video Animations with GSAP
This is what I learned while creating a video scroll effect using GreenSock Animation Platform (GSAP).
Interactive Storytelling: Scroll-Linked Video Animations with GSAP

Interactive Storytelling: Scroll-Linked Video Animations with GSAP
This is what I learned while creating a video scroll effect using GreenSock Animation Platform (GSAP).
This effect links the playback of a video to the user’s scroll position, creating a dynamic and engaging user experience.
Demo: https://fullwidth-video-scroll.vercel.app/
You can find the full code this GitHub repository: https://github.com/danielalbinsson/fullwidth-video-scroll
A key thing for making this work frame by frame with your video is to encode the video so that each frame is an I-frame. This creates a larger than typical file but is needed to this frame scrolling effect.
You can do this using FFMPG with the “-g 1” flag. “-crf” sets the quality, lower is higher quality — and larger file size.
Here is what worked well for me:
ffmpeg -i ~ video/filename.mov \
-movflags faststart \
-vcodec libx264 \
-crf 20 \
-g 1 \
-pix_fmt yuv420p \
output-filename.mp4
Setting up the Scene
First, let’s set up our React component. We’ll be using useRef to get references to our DOM elements and useEffect to initialize our GSAP animations.
Note this example is using next.js that part should not matter.
We’ll need a video element and a few text containers. The video will be pinned to the screen while the text scrolls over it.
Typescript
'use client';
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
// Register ScrollTrigger plugin
gsap.registerPlugin(ScrollTrigger);
export default function VideoScrollTextPage() {
const videoRef = useRef<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const textContainer1Ref = useRef<HTMLDivElement>(null);
const textContainer2Ref = useRef<HTMLDivElement>(null);
const textContainer3Ref = useRef<HTMLDivElement>(null);
const textContainer4Ref = useRef<HTMLDivElement>(null);
const mainRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// ... animation code will go here
}, []);
return (
<div ref={mainRef} className="bg-black">
<div
ref={containerRef}
className="h-screen bg-black flex items-center justify-center relative overflow-hidden"
>
<video
ref={videoRef}
className="w-full h-full object-cover"
muted
playsInline
preload="auto"
>
<source src="/birds.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<div
className="absolute w-full"
style={{ top: '100vh' }}
>
<div ref={textContainer1Ref} className="text-xl flex flex-col py-8 px-12 lg:px-96 text-white">
<p className="pb-4 max-w-lg">
Hope is the thing with feathers —
</p>
<p className="pb-4 max-w-lg ">
That perches in the soul —
</p>
<p className="pb-4 max-w-lg ">
And sings the tune without the words —
</p>
<p className="max-w-lg pb-4">
And never stops — at all —
</p>
</div>
<div ref={textContainer2Ref} className="text-xl flex flex-col py-8 px-12 lg:px-96 text-white">
<p className="pb-4 max-w-lg ">
And sweetest — in the Gale — is heard —
</p>
<p className="pb-4 max-w-lg ">
And sore must be the storm —
</p>
<p className="pb-4 max-w-lg ">
That could abash the little Bird
</p>
<p className="max-w-lg pb-4">
That kept so many warm —
</p>
</div>
<div ref={textContainer3Ref} className="text-xl flex flex-col py-8 px-12 lg:px-96 text-white">
<p className="pb-4 max-w-lg ">
I've heard it in the chillest land —
</p>
<p className="pb-4 max-w-lg ">
And on the strangest Sea —
</p>
<p className="pb-4 max-w-lg ">
Yet — never — in Extremity,
</p>
<p className="pb-4 max-w-lg">
It asked a crumb — of Me.
</p>
</div>
<div ref={textContainer4Ref} className="text-xl flex flex-col py-0 px-12 lg:px-96 text-white">
<p className="text-md max-w-lg">
"Hope is the Thing with Feathers"
by Emily Dickinson
</p>
</div>
</div>
</div>
{/* Fin screen */}
<div className="h-screen bg-white flex items-center justify-center">
<h1 className="text-6xl font-bold text-black">Fin</h1>
</div>
</div>
);
}
Animating with GSAP and ScrollTrigger
Now for the fun part! Inside our useEffect, we'll set up our GSAP timeline and ScrollTrigger.
const video = videoRef.current;
const container = containerRef.current;
const textContainer1 = textContainer1Ref.current;
const textContainer2 = textContainer2Ref.current;
const textContainer3 = textContainer3Ref.current;
const textContainer4 = textContainer4Ref.current;
if (!video || !container || !textContainer1 || !textContainer2 || !textContainer3 || !textContainer4) return;
const ctx = gsap.context(() => {
const handleLoadedMetadata = () => {
video.currentTime = 0;
video.pause();
const tl = gsap.timeline({
scrollTrigger: {
trigger: container,
start: 'top top',
end: '+=600',
scrub: true,
pin: true,
},
});
// Animate video playback
tl.to(video, {
currentTime: video.duration,
ease: 'none',
}, 0);
// Animate the text containers with stagger
tl.to(textContainer1, {
y: () => -(textContainer1.offsetHeight + window.innerHeight - 280),
ease: 'expo.out',
}, 0.04);
tl.to(textContainer2, {
y: () => -(textContainer2.offsetHeight + window.innerHeight - 280),
ease: 'expo.out',
}, 0.08);
tl.to(textContainer3, {
y: () => -(textContainer3.offsetHeight + window.innerHeight - 280),
ease: 'expo.out',
}, 0.1);
tl.to(textContainer4, {
y: () => -(textContainer4.offsetHeight + window.innerHeight - 100),
ease: 'expo.out',
}, 0.12);
};
if (video.readyState >= 1) {
handleLoadedMetadata();
} else {
video.addEventListener('loadedmetadata', handleLoadedMetadata, { once: true });
}
}, mainRef);
return () => ctx.revert(); if (!video || !container || !textContainer1 || !textContainer2 || !textContainer3 || !textContainer4) return;
Breaking Down the Code
**gsap.registerPlugin(ScrollTrigger):** We start by registering theScrollTriggerplugin with GSAP.**gsap.context(...):** We usegsap.context()for better cleanup in React. This ensures that our animations are properly reverted when the component unmounts.**handleLoadedMetadata:** We wrap our animation logic in a function that runs after the video's metadata has loaded. This is important because we need the video'sdurationto animate itscurrentTime.**gsap.timeline(...):** We create a GSAP timeline and attach aScrollTriggerto it.trigger: container: The animation starts when thecontainerelement enters the viewport.start: 'top top': The animation starts when the top of thecontainerhits the top of the viewport.end: '+=600': The animation lasts for 600 pixels of scroll. This number setts how long the user needs to scroll to get to the end of the video.scrub: true: This is the magic property that links the animation to the scrollbar. Instead of playing the animation once, it scrubs through it as the user scrolls.pin: true: This pins thecontainerelement to the screen while the animation is active.- Animating the Video:
tl.to(video, { currentTime: video.duration, ease: 'none' }, 0);- This is the core of the video scroll effect. We’re animating the
currentTimeof the video from 0 to its fullduration. Because we're usingscrub, the video will play as the user scrolls down and rewind as they scroll up.
Animating the Text:
- We use
tl.to()to animate theyposition of each text container. - The
yvalue is calculated dynamically to ensure the text scrolls up and out of the view. - We add a slight delay to each text animation (the last parameter in
tl.to) to create a staggered effect.
And that’s it!
With just a few lines of code, we’ve created a beautiful and interactive video scroll effect. GSAP’s ScrollTrigger makes it incredibly easy to create complex scroll-based animations.
Experiment with different easing functions, animation properties, and timings to create your own unique effects.
A special thanks to Taryn Elliott on Pexels for the video of the birds.
메타데이터
- post_id
- 87bdf8fb045b
- slug
- interactive-storytelling-scroll-linked-video-animations-with-gsap-87bdf8fb045b
- url
- https://medium.com/@i_72880/interactive-storytelling-scroll-linked-video-animations-with-gsap-87bdf8fb045b
- canonical_url
- https://medium.com/@i_72880/interactive-storytelling-scroll-linked-video-animations-with-gsap-87bdf8fb045b
- author_url
- https://medium.com/@i_72880
- status
- ok
- fetched_at
- 2026-08-02 06:19:34