Scroll Pin Steps — Free GSAP ScrollTrigger Steps Snippet
Scroll Pin Steps · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Scroll Pin Steps — A Pinned Panel That Advances as You Scroll

Scroll pin steps is the "how it works" pattern where a two-column panel sticks to the screen and its content — step number, heading, body, and visual — swaps from one step to the next as you scroll, with a progress bar tracking how far through you are. This snippet builds it with GSAP and ScrollTrigger (from a CDN), plus plain HTML and CSS.
Pin once, scroll through many steps
A single ScrollTrigger pins the panel with pin: true and runs for end: '+=300%' — three extra screens of scroll for four steps. During that pinned window, onUpdate reads self.progress (0 to 1) and maps it to a step index with Math.floor(progress × stepCount). So one pinned section presents the entire sequence without repeating markup for each step — the content is data, swapped in place.
Data-driven content
The steps live in a STEPS array of { num, head, body }. A show(i) function writes the active step's text into the panel and crossfades to the matching visual. Guarding with if (i === current) return means the swap only runs when the step actually changes, not on every scroll frame — important because onUpdate fires constantly while pinned.
Crossfading visuals and popping text
When the step changes, the visuals crossfade via opacity tweens (only the active one is shown), and the text elements pop in with a small fromTo rise-and-fade plus a stagger, using overwrite: true so rapid scrolling can't stack half-finished tweens. This gives each step a clean micro-transition instead of a hard text replacement.
The progress bar
Because onUpdate also has the continuous self.progress, the snippet sets a fill bar's width to progress × 100% every frame — a smooth, scrubbed indicator of position within the pinned section that's independent of the discrete step swaps. It gives the viewer a sense of "how much is left" while the steps tick over.
Discrete steps from a continuous scroll
The interesting part is mixing two readings of the same scroll: a continuous one for the progress bar, and a quantized one (floor) for which step is active. ScrollTrigger's single progress value drives both, so they stay perfectly in sync — the bar is exactly one-quarter full as step two begins, and so on.
Customizing it
Add steps to the array (and matching visuals) and bump the end accordingly, change the per-step transitions, swap emoji for real images or components, or flip the columns. Pair it with a scroll horizontal pin, a progress wizard, or scroll sticky stack.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to reason through the progress-to-step mapping alone. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to walk through exactly why Math.floor(self.progress * STEPS.length) is the right formula for picking the active step, or why guarding show(i) with an if (i === current) return check matters when onUpdate fires on every single scroll frame. The same assistant can help optimize it — asking whether the crossfade and pop-in tweens should use gsap.timeline with overwrite instead of individual tweens as more steps are added, or whether the progress bar write could be batched with the step-swap logic. It's also useful for extending the effect: ask it to add per-step sound cues, sync the ps-visual crossfades to real screenshots or Lottie animations, or make the panel snap so scroll settles cleanly on step boundaries. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "scroll pin steps" how-it-works panel in plain HTML, CSS, and JavaScript using GSAP with its ScrollTrigger plugin (load both from a CDN).
Requirements:
- A two-column pinned section: a left column showing a step number, heading, body text, and a thin progress-fill bar; a right column with one visual per step, all absolutely positioned on top of each other, only the first one visible at load.
- Store all step content (number, heading, body) in a plain JavaScript array of objects, not hardcoded per-step markup, so adding a step means adding one array entry.
- Create a single ScrollTrigger (not a full timeline) with pin: true, scrub: true, and an end distance long enough to give each step comfortable room (e.g. 300% of the viewport for four steps), and read self.progress inside its onUpdate callback.
- Inside onUpdate, compute the active step index as Math.floor(progress times step count), clamped to the last valid index, and only re-render the step's text and crossfade the visuals when that index actually differs from the previously active one — do not re-run the reveal animation on every scroll frame.
- When the active step changes, crossfade the visuals (fade out the previous one, fade in the new one) and animate the number/heading/body in with a small staggered rise-and-fade, using overwrite so rapid scrolling can't stack overlapping tweens.
- In that same onUpdate, independently and continuously update a progress-fill bar's width to progress times 100%, so the bar's continuous motion and the stepped content changes are both driven from the exact same progress value and can never drift apart.
- Confirm scrolling back up decrements through the steps in reverse and the bar unfills accordingly, purely because the underlying scroll position is scrubbed - no separate reverse-specific code.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
- 2Paste HTML, CSS, and JSA two-column panel renders with step one.
- 3Scroll downThe panel pins and steps advance in place.
- 4Watch the barThe progress fill tracks your position.
- 5Scroll back upSteps reverse — everything is scrubbed.
- 6Add a stepExtend the STEPS array and the end value.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A single ScrollTrigger pins the panel and runs for end: +=300%, three extra screens for four steps. In onUpdate it reads self.progress from 0 to 1 and maps it to a step index with floor(progress times stepCount). So the whole sequence plays inside one pinned section, with the content swapped in place rather than duplicated per step.
onUpdate fires on every scroll frame while pinned, but the step only changes occasionally. The if (i === current) return guard ensures the text write, visual crossfade, and pop-in tweens run only when the active step actually changes, avoiding redundant work and preventing the entrance animation from restarting every frame.
Both come from the same self.progress value. The bar width is set to progress times 100% continuously, while the active step is the floored progress times step count. Because one number drives both, the bar is exactly one quarter full as the second step begins, and so on — the continuous and quantized readings never drift.
Visuals crossfade with opacity tweens so only the active one shows, and the text pops in with a fromTo rise-and-fade. Passing overwrite: true means a new step's tween cancels any in-flight one, so rapid scrolling cannot stack half-finished animations — each step lands in a clean state.
Keep the steps array in your component and, in a mount effect, register ScrollTrigger and create the pinned trigger whose onUpdate updates state (active index and progress) or writes to refs. Return a cleanup that reverts the GSAP context so the pin is removed on unmount. Render the active step from state; the CSS ports unchanged.