Skip to content
caliper ui docs

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.

Free
pnpm dlx shadcn@latest add @caliper/toc-progress

Design system guide

Scroll the article; the outline reads along.

Overview

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.

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.

Tokens

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.

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.

Colour

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.

Type

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.

The type scale

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.

Components

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.

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.

Buttons

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.

Motion

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.

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.

Release

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.

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.

Installs components/ui/toc-progress.tsxDepends on none beyond ReactMotion honours reduced motion

When 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

terminal
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.

example.tsx
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.

PropTypeDefaultDescription
items *TocItem[]—The headings, in page order; each `id` is the heading element's id.
minutesnumber—Minutes the whole page takes to read; shows the time left when given.
labelstring"On this page"

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.

All Compare & browse examples