More Scroll Snippets
Scroll Phone Screens — Free GSAP App Showcase Snippet
Scroll Phone Screens · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Scroll Phone Screens — Scrub Through an App Inside a Pinned Device

The scroll phone screens pattern is the app-landing-page staple: a phone mockup pins in the center of the viewport, and as you scroll, the app's screens slide vertically inside the device frame — as if the page's scrollbar were your thumb flicking through the app. Captions above the phone hand off in sync with each screen. This snippet builds it with GSAP ScrollTrigger (from a CDN), a pure-CSS phone frame, and one master timeline.
The device frame is CSS, and its viewport is a clipping window
The phone is a rounded aspect-ratio: 9/19 box with an inset shadow for the bezel and an absolutely positioned notch. Inside it, .pss-viewport has overflow: hidden with the screen's corner radius — that's the clipping window. The four app screens live in a .pss-screens column set to height: 400%, so each screen fills the viewport exactly and everything outside the window is invisible. The entire effect is translating that one column.
One yPercent tween moves all four screens
The column tweens to yPercent: -75 (that is, -100 × (n−1)/n) across the pinned scroll. Because each screen is 25% of the column, every -25% step brings the next screen fully into the window. Translating a single element is cheaper than animating four separately, stays perfectly seamless at the boundaries, and — since yPercent is a transform — runs on the compositor without any layout work.
Captions are choreographed on the same timeline
Each caption owns one screen-length of the master timeline: it fades and rises in just before its screen arrives (i − 0.3) and lifts out as the next one takes over (i + 0.7). Because entrances and exits are positioned on the *same* timeline that moves the screen column, caption handoffs can never drift out of sync with the screens — scrub position is the single source of truth.
Scroll distance scales with the screen count
The trigger's end is computed as SCREENS × 90%, so adding a fifth screen automatically grants the section more scroll room and keeps the per-screen pacing constant. The duration of the column tween (SCREENS − 1) matches the caption loop's timeline positions, so the choreography scales with zero manual retuning.
scrub: 0.4 mimics a real swipe
A raw scrub: true locks the screens rigidly to the scrollbar, which feels mechanical. The 0.4-second smoothing window lets the column glide briefly after each wheel tick — close to the momentum of a real touch flick — while still settling exactly where the scrollbar says.
Why vertical screens instead of crossfades
A crossfade says "here's another screenshot"; a sliding column says "this is one continuous app." The shared column also means adjacent screens are briefly co-visible during the transition, exactly like mid-swipe on a real device — a detail crossfade approaches can't reproduce.
The bezel layers so screens slide underneath the notch
Layering order does quiet work here: the phone's bezel is an inset box-shadow (so it never affects inner layout), the screen viewport clips at its own radius inside the frame's padding, and the notch sits at z-index: 3 — above the sliding column. As screens travel, their content passes *under* the notch exactly as pixels do on a real device. If you swap in screenshots, keep their status-bar areas empty or the notch will overlap real UI; alternatively move the notch into each screen and let it travel, which reads as a scrolling screenshot instead of a live device.
Customizing it
Swap the gradient screens for real app screenshots (<img> per .pss-screen), change SCREENS, or flip the motion horizontal with xPercent and flex-direction: row. Pair it with a sticky scroll features section, a scroll zoom hero opener, or a phone mockup for static screens elsewhere on the page.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work out the column-slide math by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why one yPercent tween on the whole screens column reads correctly at every screen boundary, or how the SCREENS constant flows through the end distance, the column's target yPercent, and the caption loop's timeline positions without any of them going out of sync. The same assistant can help optimize it — asking whether the caption fade tweens should be batched or whether the notch's z-index layering could cause paint issues with real screenshots instead of gradients. It's also useful for extending the effect: ask it to add scroll-snap so screens settle on whole positions instead of stopping mid-swipe, wire in real app screenshots with lazy loading, or add a horizontal variant using xPercent and flex-direction row. 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 phone screens" app showcase in plain HTML, CSS, and JavaScript using GSAP with its ScrollTrigger plugin (load both from a CDN) — no video, no per-screen tweens.
Requirements:
- A CSS-only phone frame (rounded bezel, notch, clipped viewport) containing a single tall column of app "screens" stacked vertically, where the column's total height is 100% times the number of screens (e.g. 400% for 4 screens) and each screen is exactly 100/N percent of that column's height.
- A number of caption elements outside the phone, one per screen, absolutely positioned in the same spot, all but the first starting at opacity 0.
- Register one GSAP timeline on a single ScrollTrigger with pin: true and a scrub smoothing value (not a boolean), with the end distance computed from the screen count (e.g. count times 90%) rather than hardcoded, so adding a screen automatically extends the scroll distance.
- Inside that timeline, tween the screens column's yPercent from 0 to a single target value computed as -100 times (count - 1) divided by count, using ease none, so translating one element cycles through every screen with perfectly flush transitions.
- On that same timeline, position each caption's fade-in and fade-out (opacity plus a vertical offset) at explicit timeline positions computed from its screen index, so each caption enters just before its screen fully arrives and exits just as the next one takes over — the last caption should stay visible rather than exit.
- Do not hardcode per-screen timeline positions as separate magic numbers scattered through the code; derive them from the same loop/index so adding or removing a screen only requires changing the screen count and the data array.
- Confirm scrolling back up runs the whole sequence in reverse, mid-swipe states included, purely from the scrubbed timeline — no separate reverse code.Step by step
How to Use
- 1Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
- 2Paste HTML, CSS, and JSA CSS phone frame wraps a 400%-tall screen column.
- 3Scroll into the stageThe phone pins and screens start sliding upward.
- 4Watch the captionsHeadlines hand off in sync with each screen.
- 5Scroll back upThe app scrubs in reverse, mid-swipe included.
- 6Swap in screenshotsDrop img tags into each screen; bump SCREENS to add more.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
All four screens sit in one column set to height: 400% inside an overflow-hidden viewport. A pinned, scrubbed ScrollTrigger tweens the column to yPercent −75, and since each screen is 25% of the column, every −25% step brings the next screen fully into the window. One transform moves everything.
Caption entrances and exits are placed at explicit positions (i − 0.3 and i + 0.7) on the same master timeline that slides the column. Because both share one scrubbed timeline, the scrollbar is the single source of truth — captions can never drift relative to screens, even when scrolling erratically or reversing.
Add one .pss-screen div, then change SCREENS to 5 and .pss-screens height to 500% with each screen at 20% (or set height with a calc from a --n variable). The JS already scales: end distance, the column's yPercent target, and the caption loop all derive from the SCREENS constant, so pacing stays constant automatically.
A single yPercent tween is compositor-only, keeps adjacent screens perfectly flush during transitions (you briefly see both, like a real mid-swipe), and makes the math trivial — screen k is fully visible at −k × 25%. Independent per-screen tweens would need four synchronized animations and still couldn't guarantee seamless edges.
Yes — add snap to the ScrollTrigger config: snap: 1 / (SCREENS - 1) makes the scrub settle on the nearest screen boundary when scrolling pauses, or pass an object like { snapTo: 1/(SCREENS-1), duration: 0.3, ease: 'power1.inOut' } for tuned settle physics. Mid-swipe stopping is deliberate in the demo because it proves the column is one continuous surface, but snapping reads better when screens carry dense UI.
Render screens from an array and build the timeline in a mount effect — useEffect, onMounted, or ngAfterViewInit — using refs for the stage and column instead of selectors. Wrap setup in gsap.context and revert it in the cleanup so the pin unregisters on unmount or route change. The frame styles map cleanly to Tailwind utilities if you prefer.