The OnThisPage component renders a floating navigation panel listing headings from the current document, dynamically highlighting active sections as the user scrolls.
It uses an IntersectionObserver under the hood (via AnchorProvider) and a small linear-interpolation trick to glide a highlight indicator between the active anchors. The behavior is automatic for the most common cases — usually you pass headings and you're done.
For 95% of custom layouts, <OnThisPage.Tree headings={headings} /> is enough. Reach for the lower-level pieces only when you want full control over the positioning math.
import { OnThisPage } from 'boltdocs/primitives'
Using the primitive OnThisPage modules, you can assemble a fully custom table of contents. The example below shows how to compose a layout with custom titles, list containers, active indicator lines, and smooth scroll behaviors:
// docs/components/CustomTOC.tsx
import React, { useRef } from 'react'
import { OnThisPage, AnchorProvider, ScrollProvider } from 'boltdocs/primitives'
import { useRoutes } from 'boltdocs/client'
export default function CustomTOC() {
const { currentRoute } = useRoutes()
const scrollContainerRef = useRef<HTMLDivElement>(null)
const headings = currentRoute?.headings || []
if (headings.length === 0) return null
// Convert headings database to standard TOC structure
const tocItems = headings.map((h) => ({
title: h.text,
url: `#${h.id}`,
depth: h.level,
}))
return (
<OnThisPage className="border-l border-subtle pl-6 py-6 bg-transparent">
{/* 1. Static Title Header */}
<OnThisPage.Header className="text-xs font-bold uppercase tracking-wider text-zinc-400 mb-4">
Table of Contents
</OnThisPage.Header>
{/* 2. Scroll-Spy Context Wrapper Providers */}
<AnchorProvider toc={tocItems} single={false}>
<ScrollProvider containerRef={scrollContainerRef}>
{/* 3. Masked Scrollable Viewport */}
<OnThisPage.Content ref={scrollContainerRef} className="max-h-[80vh]">
<OnThisPage.List className="relative border-l border-zinc-200 dark:border-zinc-800">
{/* 4. Active Section Accent Indicator */}
<OnThisPage.Indicator className="bg-primary-500" />
{/* 5. Hierarchical Link Mapping */}
{headings.map((h) => (
<OnThisPage.Item key={h.id} level={h.level}>
<OnThisPage.Link href={`#${h.id}`}>
{h.text}
</OnThisPage.Link>
</OnThisPage.Item>
))}
</OnThisPage.List>
</OnThisPage.Content>
</ScrollProvider>
</AnchorProvider>
</OnThisPage>
)
}
[!TIP]
If you don't want to map items manually, you can use the high-level primitive wrapper <OnThisPage.Tree headings={headings} /> to automatically mount the observers, indicators, and list links inside an active layout.
| API | What you give up | What you keep |
|---|
<OnThisPage.Tree headings={headings} /> | Full control over scroll-math | The simplest possible integration — one prop in. |
<AnchorProvider> + <ScrollProvider> + manual <Items> | Nothing | Bespoke observer options and container placement. |
Tree is what you want unless you have a strong reason. The internals are eight lines of code total — reach for them only when you're building a sidebar that isn't a list of headings.
headings must include id. The rehype-slug pipeline assigns IDs to every heading at build time, but if you're rendering on the fly (editor previews, an inline CMS injection), make sure each heading has an anchor ID matching OnThisPage.Link's href.
ScrollProvider needs a stable ref. Pass a containerRef={useRef(null)} — don't create a new ref every render, or the auto-scroll will jump on every update.
single=true switches the scroll-spy to "first matching heading wins" mode. Useful when you have lots of h3 underneath one h1 and want the parent — not the child — to highlight.
The OnThisPage primitive provides the following sub-components for structure customization:
| Component | HTML Tag | Description | Props |
|---|
OnThisPage.Root / OnThisPage | <nav> | The outer container wrapper representing the sticky desktop right column navigation area. | ComponentBaseProps |
OnThisPage.Header | <div> | A typography title block to label the table of contents. | ComponentBaseProps |
OnThisPage.Content | <div> | Scrollable viewport container with hidden scrollbars. | OnThisPage.Content Props |
OnThisPage.List | <ul> | List element shell that wraps the item anchors. | ComponentBaseProps |
OnThisPage.Item | <li> | Single list item cell wrapper. Indents H3 elements. | OnThisPage.Item Props |
OnThisPage.Link | <a> | Anchor link element that intercepts click events to perform smooth viewport scrollings. | OnThisPage.Link Props |
OnThisPage.Indicator | <div> | A floating vertical highlight track indicator showing the active anchor link. | ComponentBaseProps |
OnThisPage.Items | Varies | Loop helper rendering active links and indicators automatically based on heading configs. | OnThisPage.Items Props |
OnThisPage.Tree | <nav> | High-level automated component grouping containing all providers, observers, and items. | OnThisPage.Tree Props |
| Property | Type | Default | Description |
|---|
children | ReactNode | undefined | Children content elements. |
className | string | undefined | Custom CSS utility classes. |
style | CSSProperties | undefined | Inline style overrides. |
| Property | Type | Default | Description |
|---|
ref | Ref<HTMLDivElement> | undefined | Ref instance for the container element. |
scrollRef | RefObject<HTMLElement> | undefined | Ref of the main viewport content element to spy scrolling on. |
| Property | Type | Default | Description |
|---|
level | number | undefined | The heading hierarchy depth. Depth of 3 triggers inner padding. |
| Property | Type | Default | Description |
|---|
href | string | Required | Element target anchor ID (e.g. '#installation'). |
active | boolean | false | Highlight active override toggle. |
onClick | (event) => void | undefined | Custom click intercept callback. |
| Property | Type | Default | Description |
|---|
headings | TOCItemType[] | Required | Raw page headers data to map. |
| Property | Type | Default | Description |
|---|
headings | TOCItemType[] | Required | Headers array mapping for active rendering. |
If you are constructing a fully bespoke tracker, you can import and wrap your layout in the custom React context providers exposed via OnThisPage:
| Context Provider | Description | Props |
|---|
AnchorProvider | Watches scroll events using IntersectionObserver and tracks the active page header. | AnchorProviderProps |
ScrollProvider | Auto-scrolls the table of contents lists to keep the active item aligned in viewport center. | ScrollProviderProps |
| Property | Type | Default | Description |
|---|
toc | TOCItemType[] | Required | Table of contents headers array to observe. |
single | boolean | false | If true, only activates one item at a time. |
observerOptions | IntersectionObserverInit | undefined | IntersectionObserver options override. |
| Property | Type | Default | Description |
|---|
containerRef | RefObject<HTMLElement> | Required | Ref of scroll container element holding the links list. |
Last updated on July 27, 2026