Search

Search the site

All components

Scroll Progress Rail

Fixed vertical scroll progress rail for long marketing and docs pages. Page-only fill or optional section markers with click-to-jump, scoped progress via scopeRef, ScrollSectionAnchor registration, and reduced-motion fallbacks.

Page progress
Page-only rail that fills as the document scrolls.
Intro
Scroll the page to watch the rail fill (1 of 5).
Page mode tracks overall document scroll without section markers. The rail stays fixed on the viewport while you move through this example.
Features
Scroll the page to watch the rail fill (2 of 5).
Page mode tracks overall document scroll without section markers. The rail stays fixed on the viewport while you move through this example.
Workflow
Scroll the page to watch the rail fill (3 of 5).
Page mode tracks overall document scroll without section markers. The rail stays fixed on the viewport while you move through this example.
Pricing
Scroll the page to watch the rail fill (4 of 5).
Page mode tracks overall document scroll without section markers. The rail stays fixed on the viewport while you move through this example.
FAQ
Scroll the page to watch the rail fill (5 of 5).
Page mode tracks overall document scroll without section markers. The rail stays fixed on the viewport while you move through this example.
1"use client";
2
3import { useRef } from "react";
4import {
5 Card,
6 CardContent,
7 CardDescription,
8 CardHeader,
9 CardTitle,
10} from "@/components/ui/card";
11import { ScrollProgressRail } from "@/registry/ui";
12
13const PAGE_BLOCKS = [
14 "Intro",
15 "Features",
16 "Workflow",
17 "Pricing",
18 "FAQ",
19] as const;
20
21export function ScrollProgressRailPageExample() {
22 const scopeRef = useRef<HTMLDivElement>(null);
23
24 return (
25 <>
26 <ScrollProgressRail scopeRef={scopeRef} side="right" />
27
28 <div
29 ref={scopeRef}
30 className="w-full rounded-xl border bg-linear-to-b from-muted/20 to-background"
31 >
32 <div className="flex flex-col gap-8 p-6 sm:p-8">
33 {PAGE_BLOCKS.map((block, index) => (
34 <Card key={block} className="min-h-56 border-border/70 bg-card/90">
35 <CardHeader>
36 <CardTitle>{block}</CardTitle>
37 <CardDescription>
38 Scroll the page to watch the rail fill ({index + 1} of{" "}
39 {PAGE_BLOCKS.length}).
40 </CardDescription>
41 </CardHeader>
42 <CardContent className="text-sm text-muted-foreground">
43 Page mode tracks overall document scroll without section
44 markers. The rail stays fixed on the viewport while you move
45 through this example.
46 </CardContent>
47 </Card>
48 ))}
49 </div>
50 </div>
51 </>
52 );
53}
Section markers
Anchored sections with labeled markers and Reveal blocks.
Scroll-aware navigation
A fixed rail that tracks page progress at a glance.
A fixed rail that tracks page progress at a glance.
Section markers
Optional dots align to anchored sections and support click-to-jump.
Optional dots align to anchored sections and support click-to-jump.
Long-form pages
Pair with reveal animations on landing and docs layouts.
Pair with reveal animations on landing and docs layouts.
Accessible by default
Progressbar semantics in page mode and labeled section controls.
Progressbar semantics in page mode and labeled section controls.
Composable backdrop
Works alongside layered wave footers and hero sections.
Works alongside layered wave footers and hero sections.
1"use client";
2
3import { useRef } from "react";
4import {
5 Card,
6 CardContent,
7 CardDescription,
8 CardHeader,
9 CardTitle,
10} from "@/components/ui/card";
11import {
12 Reveal,
13 ScrollProgressRail,
14 ScrollSectionAnchor,
15 ScrollSectionRailProvider,
16} from "@/registry/ui";
17
18const SECTION_ITEMS = [
19 {
20 id: "spr-overview",
21 label: "Overview",
22 title: "Scroll-aware navigation",
23 description: "A fixed rail that tracks page progress at a glance.",
24 },
25 {
26 id: "spr-sections",
27 label: "Sections",
28 title: "Section markers",
29 description:
30 "Optional dots align to anchored sections and support click-to-jump.",
31 },
32 {
33 id: "spr-marketing",
34 label: "Marketing",
35 title: "Long-form pages",
36 description: "Pair with reveal animations on landing and docs layouts.",
37 },
38 {
39 id: "spr-accessibility",
40 label: "Accessibility",
41 title: "Accessible by default",
42 description:
43 "Progressbar semantics in page mode and labeled section controls.",
44 },
45 {
46 id: "spr-compose",
47 label: "Compose",
48 title: "Composable backdrop",
49 description: "Works alongside layered wave footers and hero sections.",
50 },
51] as const;
52
53export function ScrollProgressRailSectionsExample() {
54 const scopeRef = useRef<HTMLDivElement>(null);
55 const overviewRef = useRef<HTMLElement>(null);
56
57 return (
58 <ScrollSectionRailProvider>
59 <ScrollProgressRail
60 scopeRef={scopeRef}
61 side="right"
62 sections={[
63 { id: "spr-overview", label: "Overview", ref: overviewRef },
64 ...SECTION_ITEMS.slice(1).map(({ id, label }) => ({ id, label })),
65 ]}
66 />
67
68 <div ref={scopeRef} className="w-full rounded-xl border bg-background">
69 <div className="flex flex-col gap-16 p-6 sm:p-8">
70 {SECTION_ITEMS.map((section, index) => {
71 const content = (
72 <Reveal animation="fade-up">
73 <Card className="min-h-52 border-border/70 bg-card/95">
74 <CardHeader>
75 <CardTitle>{section.title}</CardTitle>
76 <CardDescription>{section.description}</CardDescription>
77 </CardHeader>
78 <CardContent className="text-sm text-muted-foreground">
79 {section.description}
80 </CardContent>
81 </Card>
82 </Reveal>
83 );
84
85 if (index === 0) {
86 return (
87 <ScrollSectionAnchor
88 key={section.id}
89 sectionId={section.id}
90 ref={overviewRef}
91 className="scroll-mt-24"
92 >
93 {content}
94 </ScrollSectionAnchor>
95 );
96 }
97
98 return (
99 <ScrollSectionAnchor
100 key={section.id}
101 sectionId={section.id}
102 className="scroll-mt-24"
103 >
104 {content}
105 </ScrollSectionAnchor>
106 );
107 })}
108 </div>
109 </div>
110 </ScrollSectionRailProvider>
111 );
112}
Marketing page
Long-form layout with section rail, Reveal storytelling, and LayeredWaves footer.

Scroll progress rail

Long pages, clear orientation

Section markers on the left track hero, features, and proof blocks while readers move through the story.

Reveal as you scroll
Combine section markers with staggered reveal blocks for polished storytelling.
Built for docs and launches
Use the same primitive on changelog pages, product tours, and launch sites.

Layered waves finish the page while the rail keeps section context visible above the motion layer.

1"use client";
2
3import { useRef } from "react";
4import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5import {
6 Reveal,
7 ScrollProgressRail,
8 ScrollSectionAnchor,
9 ScrollSectionRailProvider,
10} from "@/registry/ui";
11
12const MARKETING_SECTIONS = [
13 {
14 id: "spr-hero",
15 label: "Hero",
16 title: "Ship pages that feel alive",
17 body: "Guide readers through long marketing surfaces with a subtle progress rail.",
18 },
19 {
20 id: "spr-features",
21 label: "Features",
22 title: "Reveal as you scroll",
23 body: "Combine section markers with staggered reveal blocks for polished storytelling.",
24 },
25 {
26 id: "spr-proof",
27 label: "Proof",
28 title: "Built for docs and launches",
29 body: "Use the same primitive on changelog pages, product tours, and launch sites.",
30 },
31] as const;
32
33export function ScrollProgressRailMarketingExample() {
34 const scopeRef = useRef<HTMLDivElement>(null);
35 const heroRef = useRef<HTMLElement>(null);
36
37 return (
38 <ScrollSectionRailProvider>
39 <ScrollProgressRail
40 scopeRef={scopeRef}
41 side="left"
42 sections={[
43 { id: "spr-hero", label: "Hero", ref: heroRef },
44 ...MARKETING_SECTIONS.slice(1).map(({ id, label }) => ({
45 id,
46 label,
47 })),
48 ]}
49 />
50
51 <div
52 ref={scopeRef}
53 className="w-full overflow-hidden rounded-xl border bg-background"
54 >
55 <div className="flex flex-col">
56 <ScrollSectionAnchor
57 sectionId="spr-hero"
58 ref={heroRef}
59 className="scroll-mt-24 px-6 py-16 sm:px-10 sm:py-20"
60 >
61 <Reveal animation="blur-in">
62 <div className="mx-auto max-w-2xl space-y-4 text-center">
63 <p className="text-sm font-medium uppercase tracking-[0.2em] text-primary">
64 Scroll progress rail
65 </p>
66 <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
67 Long pages, clear orientation
68 </h2>
69 <p className="text-muted-foreground text-base sm:text-lg">
70 Section markers on the left track hero, features, and proof
71 blocks while readers move through the story.
72 </p>
73 </div>
74 </Reveal>
75 </ScrollSectionAnchor>
76
77 {MARKETING_SECTIONS.slice(1).map((section) => (
78 <ScrollSectionAnchor
79 key={section.id}
80 sectionId={section.id}
81 className="scroll-mt-24 px-6 py-12 sm:px-10"
82 >
83 <Reveal animation="fade-up">
84 <Card className="mx-auto min-h-48 max-w-3xl border-border/70 bg-card/95">
85 <CardHeader>
86 <CardTitle>{section.title}</CardTitle>
87 </CardHeader>
88 <CardContent className="text-muted-foreground">
89 {section.body}
90 </CardContent>
91 </Card>
92 </Reveal>
93 </ScrollSectionAnchor>
94 ))}
95
96 <div className="mt-8 min-h-56">
97 <div className="flex min-h-56 items-center justify-center px-6 py-12 text-center">
98 <p className="max-w-lg text-sm text-muted-foreground sm:text-base">
99 Layered waves finish the page while the rail keeps section
100 context visible above the motion layer.
101 </p>
102 </div>
103 </div>
104 </div>
105 </div>
106 </ScrollSectionRailProvider>
107 );
108}

Installation & source

Install via the shadcn CLI or copy the registry files manually.

bash
npx shadcn@latest add @tt-ui/scroll-progress-rail

Props

NameTypeDefaultDescription
sectionsScrollProgressRailSection[]-Optional section markers; omit for page-only progress. Each entry accepts an optional ref or ScrollSectionAnchor registration
onSectionChange(id: string) => void-Called when the active section marker changes
activeThresholdnumber0.4Intersection ratio threshold for active section selection
side"left" | "right""right"Which viewport edge the rail sits on
fixedbooleantruePin the rail to the viewport edge
scopeRefRefObject<HTMLElement | null>-Scope scroll progress to this element (0→1 while it scrolls through the viewport)
embeddedbooleanfalseStatic rail for docs previews; skips Motion scroll tracking
classNamestring-Additional classes on the rail wrapper