Components
Toc progress
A table of contents that reads along with you: a thin rail bends through the nested headings and fills as the page scrolls, a marker rides it to the current section, and the reading time left counts down.
pnpm dlx shadcn@latest add @caliper/toc-progressWhen to use
Use it for
- Long articles, documentation pages and reports with two or three heading levels.
- Side panels beside a scrolling reader.
Skip it when
- Pages with a handful of short sections; a plain list of links is enough.
- Deeper nesting than three levels; the rail stops reading as one line.
Decisions
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Fill follows the reading line
- Progress is measured at a reading line a third of the way down, within the current section, so the fill moves smoothly rather than in jumps.
- Choosing a link moves focus
- The heading receives focus as well as the scroll, so keyboard users continue from there.
Styling
- The rail follows the nesting
- Each row's line sits at its heading's indent and bends between levels, so the outline's shape is visible at a glance.
Installation
pnpm dlx shadcn@latest add @caliper/toc-progress
Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage
This is the code behind the preview above.
import { type ReactNode } from "react" import { TocProgress, type TocItem } from "@/components/ui/toc-progress" function Panel({ title, context, className = "max-w-md", children }: { title: string; context: string; className?: string; children: ReactNode }) { return ( <section className={`grid w-full gap-5 rounded-overlay border border-border bg-background p-6 shadow-resting max-sm:p-4 ${className}`}> <div className="grid gap-1"> <p className="font-medium">{title}</p> <p className="text-sm text-muted-foreground">{context}</p> </div> {children} </section> ); } const outline: TocItem[] = [ { id: "tp-overview", label: "Overview" }, { id: "tp-tokens", label: "Tokens" }, { id: "tp-colour", label: "Colour", level: 2 }, { id: "tp-type", label: "Type", level: 2 }, { id: "tp-scale", label: "The type scale", level: 3 }, { id: "tp-components", label: "Components" }, { id: "tp-buttons", label: "Buttons", level: 2 }, { id: "tp-motion", label: "Motion" }, { id: "tp-release", label: "Release" }, ]; const prose = "Every decision here was made once, written down, and then used everywhere, so the system stays small enough to hold in your head. Where a part needed an exception, the exception became a rule for every part, or the part changed."; export function TocProgressDemo() { return ( <Panel title="Design system guide" context="Scroll the article; the outline reads along." className="max-w-3xl"> <div className="grid gap-6 sm:grid-cols-[1fr_200px]"> <article className="h-80 overflow-y-auto rounded-control border border-border p-5 max-sm:order-2"> {outline.map((item) => { const Heading = item.level === 3 ? "h4" : item.level === 2 ? "h3" : "h2"; return ( <section key={item.id} className="grid gap-2 pb-6"> <Heading id={item.id} className={item.level ? "font-medium" : "text-lg font-semibold"}> {item.label} </Heading> <p className="text-sm leading-relaxed text-muted-foreground">{prose}</p> {!item.level && <p className="text-sm leading-relaxed text-muted-foreground">{prose}</p>} </section> ); })} </article> <TocProgress items={outline} minutes={6} /> </div> </Panel> ); }
API reference
TocProgress
component
Also accepts the native props of its root element and a ref.
Accessibility
It is built on native elements and keeps aria-label, aria-hidden and aria-current in step with state.
No custom key handling; Tab and the native activation keys do the work.
Motion
It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.
Related
- Scroll spy navAn outline that follows your reading: a marker glides to the section in view, and choosing a link scrolls there and moves focus into it.
- Reading progressWhere you are in a long page, in a small pill that appears once you start scrolling: a ring that fills as you read, the section you are in and how far through you are. Press it to jump to another section.
- Docs sidebarDocumentation navigation and an on-page outline.