component
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.
Installation
File Structure
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>wrappernav— the fixed widget container (track + panel)progressCard— the rounded background behind the tracktrack— the draggable track itselftick— one decorative divider per chapter, inside the trackcursor/cursorGhost— the real and hover-preview cursor pillspanel— the per-chapter preview panel
Props
| Prop | Type | Default |
|---|---|---|
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
| Prop | Type | Default |
|---|---|---|
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.