Sticky Cards
Stack large feature cards while scrolling using Angular, GSAP and ScrollTrigger.
Demo
How it works
Each card is absolutely positioned inside a tall scrollable track. As the user scrolls, a single GSAP timeline with a ScrollTrigger maps scroll progress to the cards' yPercent, opacity and scale. Earlier cards scale down and fade while the next card slides up from below, producing a stacked, sticky feel without relying on CSS position: sticky.
The first card is visible at scroll position 0 so the demo never looks empty. As the user scrolls, the second and third cards slide up and take over the viewport.
The demo is self-contained: the ScrollTrigger uses the scroller option to watch the demo viewport instead of the page, so it works inside the showcase without page-level scroll.
The Angular way
- GSAP and ScrollTrigger are lazy-loaded inside
ngAfterViewInitand guarded byisPlatformBrowserso SSR never runs browser-only code. gsap.context()scopes all selectors to the component host, making cleanup reliable.DestroyReftears down the context and kills every ScrollTrigger when the route changes.prefers-reduced-motionbypasses the scroll-driven animation and renders the cards in a normal stacked layout.
Source code
<div
class="sticky-cards-viewport relative h-[28rem] w-full overflow-y-auto rounded-2xl border border-border bg-card/50"
[class.reduced-motion]="reducedMotion()"
>
<div class="sticky-cards-track relative min-h-[400%] w-full">
@for (card of cards; track card.id) {
<div
class="sticky-card absolute inset-0 flex items-center justify-center p-6 md:p-10"
[class.theme-dark]="card.theme === 'dark'"
[class.theme-light]="card.theme === 'light'"
[class.theme-blue]="card.theme === 'blue'"
>
<div class="card-inner ...">
<span class="icon">{{ card.icon }}</span>
<h3>{{ card.title }}</h3>
<p>{{ card.body }}</p>
<button type="button">Learn more</button>
</div>
</div>
}
</div>
</div>Implementation recipe
- Create a standalone Angular component with a tall scrollable container and a track with enough height.
- Add the card markup. Use absolute positioning and z-index so cards can stack visually.
- Lazy-load
gsapandgsap/ScrollTriggeronly in the browser. - Use
gsap.timeline({ scrollTrigger: { ... } })withscrub: 1and the local viewport asscroller. - Make the first card visible at scroll 0, then animate subsequent cards in while scaling/fading the previous ones.
- Add a
prefers-reduced-motionfallback that shows the cards stacked without animation. - Clean up with
gsap.context().revert()and kill all ScrollTriggers on destroy.
Accessibility notes
- prefers-reduced-motion shows a normal stacked layout
- all content is readable without scroll animation
- keyboard focus order follows DOM order
Performance notes
- animate transform and opacity only
- kill ScrollTriggers on destroy to avoid leaks
- use gsap.context to scope selectors
Common pitfalls
- Letting ScrollTrigger attach to the window when the demo lives inside a scrollable container — always pass the correct
scroller. - Forgetting to kill ScrollTriggers on route change, which causes errors and leaked listeners.
- Animating
top,leftor layout properties instead oftransformandopacity. - Starting every card with
opacity: 0so the demo looks empty before the user interacts.