caliper uidocs

Scroll spy nav

Free

An outline that follows your reading: a marker glides to the section in view, and choosing a link scrolls there and moves focus into it.

Installs components/ui/scroll-spy-nav.tsxDepends on none beyond ReactMotion honours reduced motion
pnpm dlx shadcn@latest add https://caliperui.site/r/scroll-spy-nav.json

Overview

We keep only what the product needs to work: your email, your team and what you create. We never sell it, and we delete it within 30 days of you asking. Backups roll off after 35 days.

Requests go to privacy@halden.example and are answered within a week.

Data we keep

We keep only what the product needs to work: your email, your team and what you create. We never sell it, and we delete it within 30 days of you asking. Backups roll off after 35 days.

Requests go to privacy@halden.example and are answered within a week.

Retention

We keep only what the product needs to work: your email, your team and what you create. We never sell it, and we delete it within 30 days of you asking. Backups roll off after 35 days.

Requests go to privacy@halden.example and are answered within a week.

Sharing

We keep only what the product needs to work: your email, your team and what you create. We never sell it, and we delete it within 30 days of you asking. Backups roll off after 35 days.

Requests go to privacy@halden.example and are answered within a week.

Your rights

We keep only what the product needs to work: your email, your team and what you create. We never sell it, and we delete it within 30 days of you asking. Backups roll off after 35 days.

Requests go to privacy@halden.example and are answered within a week.

When to use #

Use it for

  • Docs, policies and long settings pages.
  • Any page with more than four sections.

Skip it when

  • Short pages; the outline is noise.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/scroll-spy-nav.json

Adds its 2 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 { useRef } from "react"
import { ScrollSpyNav } from "@/components/ui/scroll-spy-nav"

export function ScrollSpyNavDemo() {
  const scroller = useRef<HTMLDivElement>(null);
  const sections = [
    { id: "spy-overview", label: "Overview" },
    { id: "spy-data", label: "Data we keep" },
    { id: "spy-retention", label: "Retention", level: 2 as const },
    { id: "spy-sharing", label: "Sharing" },
    { id: "spy-rights", label: "Your rights" },
  ];
  return (
    <div className="grid w-full max-w-2xl grid-cols-[minmax(0,1fr)_160px] gap-6 max-sm:grid-cols-1">
      <div ref={scroller} className="h-[360px] overflow-y-auto rounded-[14px] p-5 shadow-[inset_0_0_0_1px_var(--border)]" tabIndex={0} role="region" aria-label="Privacy policy">
        {sections.map((section) => (
          <section key={section.id} id={section.id} className="mb-8 scroll-mt-4 outline-none">
            <h3 className="mb-2 text-sm font-semibold">{section.label}</h3>
            <p className="text-sm leading-6 text-muted-foreground">We keep only what the product needs to work: your email, your team and what you create. We never sell it, and we delete it within 30 days of you asking. Backups roll off after 35 days.</p>
            <p className="mt-2 text-sm leading-6 text-muted-foreground">Requests go to privacy@halden.example and are answered within a week.</p>
          </section>
        ))}
        <div className="h-40" aria-hidden="true" />
      </div>
      <ScrollSpyNav items={sections} scrollRoot={scroller} offset={24} className="max-sm:hidden" />
    </div>
  );
}

API reference #

ScrollSpyNav

component
PropTypeDefaultDescription
items *readonly SpyItem[]—
labelstring"On this page"
offsetnumber96Pixels from the top at which a section counts as current.
scrollRootRefObject<HTMLElement | null>—The element the sections scroll in, if not the window.
classNamestring—

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.