Shots in this scene
ScrollTrigger
In one line
ScrollTrigger ties a GSAP animation to scroll positions: it measures where an element's start and end are, then plays, scrubs or pins as the scroll bar passes them. It must re-measure whenever the layout changes.
Think of it like this
A film strip pulled through a projector by hand. Pull a little and the picture moves a little; stop and it stops; pull back and it runs backward. The scroll bar is the hand, and scrub makes the animation follow it frame for frame.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- ScrollTrigger
- A GSAP plugin that links animations to scroll positions: play, scrub, pin, snap.
- scrub
- Make the animation's progress follow the scroll position, forward and backward.
- pin
- Hold an element still on screen while the scroll passes through a range.
- pin spacer
- The wrapper
div.pin-spacerScrollTrigger puts around a pinned element so the layout keeps the room the pin needs.
Step by step
01Read the start and end numbers
Open "Scrub in a scroll box". The console says start 72 end 178 scrollable 250. The box's top is 160 px down the scroller, which is 110 px tall. The 80% line is at 88 px, so the box reaches it after 160 − 88 = 72 px of scrolling: that is start. The box's bottom (200 px) meets the 20% line (22 px) at 178: that is end.
02Scrub it by hand
Drag the scroll box's scroll bar slowly. The Volt label slides right and turns as you drag, and the readout moves from 0 to 1. At the middle of the range (scrollTop 125) the progress is 0.50, the box is 80 px along and turned half a turn, and scrolling back runs it backward. The coloured marker lines inside the scroller are the start and end.
03Pin the battery
In "Pin and the spacer" the console shows the section's parent change from scroller to pin-spacer, and the spacer is 260 px tall for a 60 px section: the 200 px you asked for is added. Scroll to 180: the section is held at the top (pinned: yes, moved by a 70 px transform inside this scroll box) and the meter is 35% full: the pin starts at scrollTop 110, and 70 ÷ 200 = 0.35.
04Let the layout move
In "Late layout" a photo above the box grows by 100 px. Before refresh(), ScrollTrigger still believes the start is 48, but the box now reaches the line at 148: scrolling to 48 would play the animation 100 px too early. After ScrollTrigger.refresh() both numbers agree. The example does this once by itself, 0.6 s after load (the three console lines). Press Photo loads for another 100 px: the line under the buttons then says start 148 | … scrollTop 248, and after refresh() it says start 248 | … 248.
Try it yourself
- 1
Start the scrub later
In "Scrub in a scroll box" change
start: "top 80%"tostart: "top 50%". The console now saysstart 105: the 50% line is at 55 px, and 160 − 55 = 105. The animation covers less scrolling, so it moves faster per pixel. - 2
Turn the pin spacing off
In "Pin and the spacer" add
pinSpacing: falseafterpin: true, then scroll to 180. The spacer is only 60 px tall and the scroll range falls from 370 to 170. The next block ("After the battery") now slides up and draws over the pinned section.
Code & diagrams
Scroll the box. `scroller` points ScrollTrigger at this box instead of the page, so the example works in the small viewfinder. Turn the timeline's speed down and scroll slowly to see the scrub.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
start 72 end 178 scrollable 250The battery section is pinned for 200 px of scrolling while its meter fills. The console shows what ScrollTrigger did to the DOM.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
parent before: scroller | parent now: pin-spacer
section height: 60 | spacer height: 260
scroll range: 370An image arrives above the box after setup. Read the three console lines (they print 0.6 s after load), or press the buttons yourself.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
at load: start 48 | the box really reaches the 90% line at scrollTop 48
photo loaded: start 48 | the box really reaches the 90% line at scrollTop 148
after refresh: start 148 | the box really reaches the 90% line at scrollTop 148Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Create the trigger before the layout is final
In "Late layout" wait for the three console lines, press Photo loads (+100 px) and read the line under the buttons, without pressing refresh().
Break #2
Pin a section near the end of the page
Put a pinned section (start: "top top", pin: true, a meter scrubbed across the pin) near the end of a page with only a short footer after it. Test page: 400 px tall viewport, a 120 px section, a 100 px footer.
Myth vs fact
Myth
ScrollTrigger always keeps itself up to date.
Fact
It refreshes on page load, on resize and when a hidden tab returns with a different window size, and nothing else. A layout change above a trigger after that (an image, a font, an accordion) needs ScrollTrigger.refresh().
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
ScrollTrigger.refresh()is not free: it reverts every pin and trigger, measures, and applies them again. Creating a pin queues one batched refresh (a pin can move other triggers), andinvalidateOnRefresh: truemakes an animation read its start values again on each refresh. Refresh once after a batch of layout changes.
Remember this
- 1
The problem this solves: CSS scroll-driven animation (Phase 12) covers many cases, but it needs a browser that supports it, has no
pinoption (in CSS you hold the element yourself withposition: sticky), and gives you no callbacks. ScrollTrigger works in every browser GSAP does, adds pinning, snapping and callbacks, and lets one timeline be played, scrubbed or toggled by scroll. - 2
Start and end. Both are written as
"<trigger position> <scroller position>":start: "top 80%"means "when the top of the trigger meets the 80% line of the scroller", andend: "bottom 20%"is the trigger's bottom reaching the 20% line.end: "+=300"means 300 px after the start. Positions can betop,center,bottom, a percentage or pixels. ScrollTrigger turns them into scroll offsets (st.start,st.end). - 3
Three modes. With
toggleActions(the default,"play none none none") the animation plays on enter, as in "play once when it comes into view". Withscrub: truethe animation's progress equals the scroll progress between start and end, forward and backward;scrub: 0.5lets it catch up half a second behind. Withpin: truethe element is held still while the scroll passes through the range.toggleActionsis ignored whenscrubis set. - 4
The pin spacer. To hold an element still, ScrollTrigger wraps it in a
div.pin-spacerthat takes its place in the layout and is made taller by the pin distance (pinSpacing, on by default), so the content below starts after the pinned part. While pinned, the element getsposition: fixedwhen the scroller is the page, or a transform inside any other scroll box. - 5
Measuring is a snapshot. ScrollTrigger reads positions when it is created (in a batch, on the next frame), again on
DOMContentLoadedandload, on windowresize(0.2 s after the last one), when a hidden tab comes back with a different window size, and when you callScrollTrigger.refresh(). It does not watch the content: an image that loads above the trigger, a font swap or an opening accordion moves everything, and the stored numbers stay stale until a refresh. - 6
On touch devices. Phone browsers resize the window when their address bar slides away. On touch devices ScrollTrigger ignores a resize that keeps the width and changes the height by under a quarter (
ignoreMobileResize), so the sliding address bar does not refresh it. A pin also takes a phone's whole screen: the build turns it off below 700 px.
Explain it without notes
How is the start of a ScrollTrigger calculated?
What does the pin spacer do and what happens with pinSpacing: false?
Practice
Write a ScrollTrigger on #panel that plays a fade-in once when the panel's top reaches 85% of the viewport, and never replays.
Pin #story for 500 px of scrolling and scrub a progress bar #bar (scaleX 0 to 1) across the same range.
Done when you can
You can write
startandendand say what number they becomeYou can scrub, toggle and pin, and say what
toggleActionsdoes when scrub is setYou can say what the pin spacer is and what
pinSpacing: falsedoes