component

Scroll Chapters

A draggable, chaptered progress-bar nav — two-way cursor/scroll sync, ghost-cursor hover preview, snap-to-step seeking, and a per-chapter panel that can opt in or out of showing.

Made by Axyl

Installation

File Structure

scroll-chapters.tsx
use-prefers-reduced-motion.tsx
scroll-trigger-utils.tsx

Usage

Basic

import { ScrollChapters } from "@/components/sora-ui/effects/scroll-chapters";

const chapters = [
  {
    id: "intro",
    content: (
      <section className="flex h-svh flex-col justify-center px-16">
        <h3>Grab the cursor</h3>
        <p>The pill on the track is a real Draggable instance.</p>
      </section>
    ),
    preview: <pre><code>{`Draggable.create(cursor, { ... })`}</code></pre>,
  },
  {
    id: "outro",
    content: (
      <section className="flex h-svh flex-col justify-center px-16">
        <h3>No panel here</h3>
        <p>Omit `preview` to opt a chapter out of the panel entirely.</p>
      </section>
    ),
    // no `preview` — this chapter never shows a panel
  },
];

export default function Page() {
  return <ScrollChapters chapters={chapters} />;
}

Each chapter's content is fully your own markup — ScrollChapters only owns the track/cursor/panel widget, not typography or layout. Give every chapter's content the same height (e.g. h-svh) so the per-chapter active window stays evenly spaced.

Hiding the widget near the edges

The whole nav (track + cursor + panel) fades out near the very start/end of the content by default — hideCardAtStart/hideCardAtEnd (and their startThreshold/endThreshold progress values) control that independently:

<ScrollChapters
  chapters={chapters}
  hideCardAtEnd={false}
  hideCardAtStart={false}
/>

Docs / catalog preview

embedded + containerQuery switch chapter sizing from 100svh to a 100cqh calc so the widget fits a bounded preview panel instead of the full viewport — pass scroller (the panel's own scroll container) when using it this way, same contract as StickyScrollCards/ScrollGallery:

<ScrollChapters
  chapters={chapters}
  containerQuery
  embedded
  scroller={viewport}
/>

Styling

No default colors

ScrollChapters ships structural Tailwind classes only (position, sizing, flex layout) — no background/text colors. Everything visible (track background, cursor color, panel background) comes from the classNames prop, same as this page's own preview.

Track ruler

The fine tick-mark ruler on the track is an inline background-image (not expressible as a single Tailwind utility) — override it entirely via classNames.track if you want a different pattern.

Class slots

className styles the root section. Pass any of these to classNames to override an inner layer:

  • chapter — each chapter's <article> wrapper
  • nav — the fixed widget container (track + panel)
  • progressCard — the rounded background behind the track
  • track — the draggable track itself
  • tick — one decorative divider per chapter, inside the track
  • cursor / cursorGhost — the real and hover-preview cursor pills
  • panel — the per-chapter preview panel

Props

PropTypeDefault
chapters?
ScrollChaptersChapter[]
-
hideCardAtStart?
boolean
true
hideCardAtEnd?
boolean
true
startThreshold?
number
0.02
endThreshold?
number
0.98
embedded?
boolean
false
containerQuery?
boolean
false
scroller?
Element | Window
-
refreshPriority?
number
-1
className?
string
-
classNames?
ScrollChaptersClassNames
-

ScrollChaptersChapter

PropTypeDefault
id?
string
-
content?
ReactNode
-
preview?
ReactNode
-

When to Use

ScrollChapters is a companion, not a hero — it's a quiet navigation aid for a page that already has its own big scroll-driven moments (text reveals, pinned sections, SVG morphs), letting a visitor see where they are and jump around without pulling focus. It doesn't carry a strong visual "wow" on its own; pair it with heavier primitives for that.

Accessibility

The drag handle and track are pointer-only (matching the original design this reimplements). When prefers-reduced-motion: reduce is set, GSAP skips creating the Draggable/ScrollTrigger instances entirely and the nav stays hidden — chapter content remains fully readable either way.

Credits

Inspired by Anime.js. Reimplemented for GSAP and vanilla JS.

Scroll to drag the cursor and flip chapters