Scroll Accordion — Free GSAP Pinned Panels Snippet

Scroll Accordion · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Scroll-driven panels
Scroll position decides the open panel.
Paired hand-offs
Open and close tweens share a position.
Stable height
Fixed OPEN_H keeps the pinned list steady.
Toggle-safe calls
classList.toggle survives reverse scrubs.
Count-aware pacing
End distance scales with panel count.
Clipped collapse
overflow hidden crops text during close.
Progress by design
The open panel doubles as a progress cue.
Reversible
Scrolling up steps back through panels.

About this UI Snippet

Scroll Accordion — Panels That Open and Close Themselves on Scroll

Screenshot of the Scroll Accordion snippet rendered live

The scroll accordion replaces clicking with scrolling: the list pins in the viewport, and each segment of scroll expands the next panel while collapsing the previous one — a self-guiding tour through a process or feature list where the reader's scroll position *is* the open panel. Scrolling up walks backward through the panels. This snippet builds it with GSAP ScrollTrigger (from a CDN) and a paired open/close choreography.

Open and close are paired at the same timeline position

For each panel i, two tweens are placed at position i on the master timeline: panel i animates to its open height while panel i − 1 animates back to zero. Because they share a position and duration, the hand-off is a perfect crossfade of heights — the list's total height stays nearly constant during transitions, so the pinned stage never visibly grows or shifts vertically mid-scroll.

A fixed open height keeps the scrub honest

The bodies open to a constant OPEN_H of 96px rather than height: 'auto'. Auto heights force GSAP to measure and lock a pixel value the first time the tween runs — fine for click accordions, but under a scrub that measurement can happen mid-gesture and produce a subtle pop. A fixed height (sized to fit the longest paragraph) makes every hand-off geometrically identical and perfectly reversible. If your content varies a lot, measure each body's scrollHeight up front and tween to that stored number instead.

Class toggles ride the timeline with .call()

The border highlight and chevron rotation are CSS states, not tweens, so they're flipped by tl.call() entries placed just after each hand-off starts. Crucially, the callbacks use classList.toggle rather than add/remove: a scrubbed timeline re-fires calls when the playhead crosses them in either direction, and a toggle self-inverts on reverse passes, keeping the visual state correct no matter how erratically the user scrolls.

Scroll distance scales with the panel count

The trigger's end is items.length × 70%, granting each panel the same scroll budget regardless of how many you add. Each open/close pair occupies one unit of timeline duration, so pacing stays even — panel three doesn't rush by faster than panel one.

Why height animation is safe here

Height tweens normally cause layout thrash, but this accordion animates one (or two) small bodies inside a pinned stage with everything else static — the browser relayouts a 600px-wide column, not the page. overflow: hidden on the bodies clips the text during the collapse, and animating opacity alongside height keeps half-open text from looking cropped.

The reader can't get lost

Because exactly one panel is open at any scroll position and the mapping is deterministic, the accordion doubles as a progress indicator: seeing "03 Measure" open tells you you're 60% through the section. That's the advantage over click accordions in narrative contexts — no decision fatigue, no skipped steps.

Customizing it

Add panels (the math adapts), swap the fixed height for measured scrollHeight, or put media in the bodies. For a click-driven version see the accordion FAQ; pair this one with scroll pin steps for a panel-swap cousin or a scroll timeline dots rail alongside.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not have to reconstruct the timeline choreography in your head. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the open tween for panel i and the close tween for panel i minus 1 are placed at the same timeline position, and why the code uses classList.toggle inside tl.call instead of classList.add or remove. The same assistant is useful for optimizing it — ask whether the fixed OPEN_H constant is the right tradeoff for content with wildly varying paragraph lengths, or how you'd measure each body's scrollHeight once up front and tween to those stored values instead. It is just as useful for extending the effect — ask it to let two panels stay open simultaneously instead of strictly one, add a progress rail that mirrors which panel index is currently open, or make the panel content itself (an image or a stat) crossfade in sync with the height tween. 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:

text
Build a "scroll accordion" in plain HTML, CSS, and JavaScript using GSAP and its ScrollTrigger plugin (load both from a CDN, no build step).

Requirements:
- A pinned list of accordion items, each with a header (index, title, chevron icon) and a collapsible body that starts at height 0 and opacity 0 with overflow hidden.
- Register a single GSAP timeline on one ScrollTrigger for the whole list, with pin: true, scrub enabled, and an end distance that scales with the number of items (for example items.length times a fixed percentage), so scroll pacing per panel stays even regardless of how many items exist.
- For every item at index i, place an open tween (animate that item's body to a fixed open height and opacity 1) at timeline position i, and — for every index after the first — place a close tween for the previous item's body (animate its height and opacity back to 0) at that same timeline position i, so each hand-off is a paired crossfade rather than two independent transitions.
- Use a fixed pixel height for the open state (not height: auto), sized to comfortably fit the longest body's content.
- Toggle each item's "open" visual state (border highlight, chevron rotation) using classList.toggle inside a tl.call placed just after each hand-off begins — not classList.add/remove — so the state self-inverts correctly when the scrubbed timeline is crossed in the reverse (scrolling up) direction.
- Confirm that scrolling backward through the pinned section correctly walks the accordion open/close sequence in reverse with no separate reverse-scroll logic, relying entirely on the scrubbed timeline's natural reversibility.

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

  1. 1
    Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
  2. 2
    Paste HTML, CSS, and JSFour closed accordion rows render in a pinned list.
  3. 3
    Scroll into the stagePanel 01 expands as the section pins.
  4. 4
    Keep scrollingEach segment opens the next panel, closing the last.
  5. 5
    Scroll back upThe accordion walks backward through its panels.
  6. 6
    Add your contentDuplicate a row; the timeline scales automatically.

Real-world uses

Common Use Cases

Process walkthroughs
Design → build → measure → scale narratives; add a scroll timeline dots rail beside it.
Feature tours
One panel per feature, then a scroll reveal grid of the rest.
How-it-works sections
Self-advancing steps beat click-to-open; compare scroll pin steps.
FAQ storytelling
Walk the top questions in order; deep-link the full list to an accordion FAQ.
Changelogs
Expand release highlights version by version, like a scrollable changelog feed.
Onboarding previews
Preview setup stages before signup; follow with sticky scroll features.
Related: Scroll Blur Focus
See the Scroll Blur Focus for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A pinned, scrubbed timeline places an open tween for panel i and a close tween for panel i − 1 at the same position i. As the scrubbed playhead moves through positions 0–3, exactly one panel is open at any scroll offset, and moving backward replays the hand-offs in reverse. Scroll position maps deterministically to accordion state.

height: 'auto' makes GSAP measure and lock a pixel value the first time the tween runs, which under a scrub can happen mid-gesture and pop. A constant OPEN_H makes every hand-off geometrically identical and cleanly reversible. For varied content, measure each body's scrollHeight once up front and tween to those stored values.

They're flipped by tl.call entries using classList.toggle rather than add/remove. A scrubbed timeline re-fires call() whenever the playhead crosses it in either direction, and a toggle self-inverts on the reverse pass — so the is-open class tracks the true state no matter how erratically the user scrubs back and forth.

Height tweens do trigger layout, but here only one small body (two during hand-offs) inside a pinned 620px column changes — the rest of the page is static, so the relayout cost is tiny. The paired open/close also keeps total list height nearly constant, which is why the pinned stage doesn't visibly shift during transitions.

Render rows from an array and build the timeline in a mount effect (useEffect, onMounted, or ngAfterViewInit) inside gsap.context scoped to a list ref, reverting it on cleanup so the pin unregisters. Keep is-open as classes toggled by the timeline rather than framework state — routing it through state would re-render on every reverse crossing. Row styling converts directly to Tailwind utilities.