Buttons
Data Display
Layout
Navigation
Table of contents
List the headings on a page and highlight the ones in view as the reader scrolls.
React
Built on Ark UI's Toc. It watches the headings you pass as items and marks the links of the ones in view as active.
Toc.Indicator follows the active links, and variant picks how it looks. The table of contents on the right of this
page is one, with the track variant.
Usage
import { Toc } from "@isbatak/react-ui/toc"
import { useRef } from "react"
import { Grid } from "styled-system/jsx"
import { DemoArticle } from "./demo-article"
const items = [
{ value: "toc-basic-introduction", depth: 1 },
{ value: "toc-basic-installation", depth: 1 },
{ value: "toc-basic-usage", depth: 1 },
{ value: "toc-basic-props", depth: 2 },
{ value: "toc-basic-events", depth: 2 },
{ value: "toc-basic-styling", depth: 1 },
]
export function TocDemo() {
const ref = useRef<HTMLDivElement>(null)
const getScrollEl = () => ref.current
return (
<Grid gridTemplateColumns="minmax(0, 1fr) 11rem" gap="8" w="full">
<DemoArticle prefix="toc-basic" ref={ref} />
<Toc.Root items={items} scrollEl={getScrollEl}>
<Toc.Title>On this page</Toc.Title>
<Toc.List>
<Toc.Indicator />
<Toc.Item item={{ value: "toc-basic-introduction", depth: 1 }}>
<Toc.Link href="#toc-basic-introduction">Introduction</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-installation", depth: 1 }}>
<Toc.Link href="#toc-basic-installation">Installation</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-usage", depth: 1 }}>
<Toc.Link href="#toc-basic-usage">Usage</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-props", depth: 2 }}>
<Toc.Link href="#toc-basic-props">Props</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-events", depth: 2 }}>
<Toc.Link href="#toc-basic-events">Events</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-styling", depth: 1 }}>
<Toc.Link href="#toc-basic-styling">Styling</Toc.Link>
</Toc.Item>
</Toc.List>
</Toc.Root>
</Grid>
)
}<Toc.Root items={items}>
<Toc.Title />
<Toc.List>
<Toc.Track />
<Toc.Indicator />
<Toc.Item item={item}>
<Toc.Link href={`#${item.value}`} />
</Toc.Item>
</Toc.List>
</Toc.Root>Each item's value is the id of its heading, and depth is its level, starting at 1. The recipe indents a link by
--toc-indent for every level below the first.
When the headings scroll inside an element rather than the page, pass scrollEl to Toc.Root, and to Toc.Track so it
follows the same scroll. The line and track examples scroll inside the preview.
Variants
Line
line is the default. The list has a border at the start, and a bar on it covers the active links.
Introduction
The table of contents follows the headings as you scroll. The entry for the heading at the top of the article is active, and the indicator moves to it.
Installation
Add the component to your project, then pass the headings as items. Each item needs the id of its heading as its value and a depth for its level.
Usage
Render a link for every item. Clicking a link scrolls its heading into view and updates the URL hash.
Props
Pass scrollEl when the headings scroll inside an element rather than the page, as they do here.
Events
onActiveChange reports the active headings whenever they change.
Styling
The recipe indents nested items by their depth and colors the active link.
On this page
import { Toc } from "@isbatak/react-ui/toc"
import { useRef } from "react"
import { Grid } from "styled-system/jsx"
import { DemoArticle } from "./demo-article"
const items = [
{ value: "toc-basic-introduction", depth: 1 },
{ value: "toc-basic-installation", depth: 1 },
{ value: "toc-basic-usage", depth: 1 },
{ value: "toc-basic-props", depth: 2 },
{ value: "toc-basic-events", depth: 2 },
{ value: "toc-basic-styling", depth: 1 },
]
export function TocDemo() {
const ref = useRef<HTMLDivElement>(null)
const getScrollEl = () => ref.current
return (
<Grid gridTemplateColumns="minmax(0, 1fr) 11rem" gap="8" w="full">
<DemoArticle prefix="toc-basic" ref={ref} />
<Toc.Root items={items} scrollEl={getScrollEl}>
<Toc.Title>On this page</Toc.Title>
<Toc.List>
<Toc.Indicator />
<Toc.Item item={{ value: "toc-basic-introduction", depth: 1 }}>
<Toc.Link href="#toc-basic-introduction">Introduction</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-installation", depth: 1 }}>
<Toc.Link href="#toc-basic-installation">Installation</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-usage", depth: 1 }}>
<Toc.Link href="#toc-basic-usage">Usage</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-props", depth: 2 }}>
<Toc.Link href="#toc-basic-props">Props</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-events", depth: 2 }}>
<Toc.Link href="#toc-basic-events">Events</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-basic-styling", depth: 1 }}>
<Toc.Link href="#toc-basic-styling">Styling</Toc.Link>
</Toc.Item>
</Toc.List>
</Toc.Root>
</Grid>
)
}Track
track draws the line with Toc.Track, an SVG that steps in for nested items with a short curve and back out after
them. The stretch beside the active links is drawn in the text color, and the indicator is a dot at its end: at the
bottom while the reader scrolls down, at the top while they scroll up.
Introduction
The table of contents follows the headings as you scroll. The entry for the heading at the top of the article is active, and the indicator moves to it.
Installation
Add the component to your project, then pass the headings as items. Each item needs the id of its heading as its value and a depth for its level.
Usage
Render a link for every item. Clicking a link scrolls its heading into view and updates the URL hash.
Props
Pass scrollEl when the headings scroll inside an element rather than the page, as they do here.
Events
onActiveChange reports the active headings whenever they change.
Styling
The recipe indents nested items by their depth and colors the active link.
import { Toc } from "@isbatak/react-ui/toc"
import { useRef } from "react"
import { Grid } from "styled-system/jsx"
import { DemoArticle } from "./demo-article"
const items = [
{ value: "toc-track-introduction", depth: 1 },
{ value: "toc-track-installation", depth: 1 },
{ value: "toc-track-usage", depth: 1 },
{ value: "toc-track-props", depth: 2 },
{ value: "toc-track-events", depth: 2 },
{ value: "toc-track-styling", depth: 1 },
]
export function TocTrackDemo() {
const ref = useRef<HTMLDivElement>(null)
const getScrollEl = () => ref.current
return (
<Grid gridTemplateColumns="minmax(0, 1fr) 11rem" gap="8" w="full">
<DemoArticle prefix="toc-track" ref={ref} />
<Toc.Root items={items} scrollEl={getScrollEl} variant="track">
<Toc.List>
<Toc.Track scrollEl={getScrollEl} />
<Toc.Indicator />
<Toc.Item item={{ value: "toc-track-introduction", depth: 1 }}>
<Toc.Link href="#toc-track-introduction">Introduction</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-track-installation", depth: 1 }}>
<Toc.Link href="#toc-track-installation">Installation</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-track-usage", depth: 1 }}>
<Toc.Link href="#toc-track-usage">Usage</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-track-props", depth: 2 }}>
<Toc.Link href="#toc-track-props">Props</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-track-events", depth: 2 }}>
<Toc.Link href="#toc-track-events">Events</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-track-styling", depth: 1 }}>
<Toc.Link href="#toc-track-styling">Styling</Toc.Link>
</Toc.Item>
</Toc.List>
</Toc.Root>
</Grid>
)
}Toc.Track measures the items, so render it inside Toc.List before the items. The line variant hides it, so it is
safe to leave in when you switch variants.
Sizes
size sets the text size and spacing of the links: sm or md, the default. These two have no headings to watch, so
defaultActiveIds sets the active link.
import { Toc } from "@isbatak/react-ui/toc"
import { Grid } from "styled-system/jsx"
const items = [
{ value: "toc-sizes-introduction", depth: 1 },
{ value: "toc-sizes-installation", depth: 1 },
{ value: "toc-sizes-usage", depth: 1 },
{ value: "toc-sizes-props", depth: 2 },
{ value: "toc-sizes-events", depth: 2 },
{ value: "toc-sizes-styling", depth: 1 },
]
export function TocSizesDemo() {
return (
<Grid gridTemplateColumns="repeat(2, 11rem)" gap="10">
<Toc.Root items={items} defaultActiveIds={["toc-sizes-props"]} size="sm">
<Toc.Title>Small</Toc.Title>
<Toc.List>
<Toc.Indicator />
<Toc.Item item={{ value: "toc-sizes-introduction", depth: 1 }}>
<Toc.Link href="#toc-sizes-introduction">Introduction</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-installation", depth: 1 }}>
<Toc.Link href="#toc-sizes-installation">Installation</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-usage", depth: 1 }}>
<Toc.Link href="#toc-sizes-usage">Usage</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-props", depth: 2 }}>
<Toc.Link href="#toc-sizes-props">Props</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-events", depth: 2 }}>
<Toc.Link href="#toc-sizes-events">Events</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-styling", depth: 1 }}>
<Toc.Link href="#toc-sizes-styling">Styling</Toc.Link>
</Toc.Item>
</Toc.List>
</Toc.Root>
<Toc.Root items={items} defaultActiveIds={["toc-sizes-props"]} size="md">
<Toc.Title>Medium</Toc.Title>
<Toc.List>
<Toc.Indicator />
<Toc.Item item={{ value: "toc-sizes-introduction", depth: 1 }}>
<Toc.Link href="#toc-sizes-introduction">Introduction</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-installation", depth: 1 }}>
<Toc.Link href="#toc-sizes-installation">Installation</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-usage", depth: 1 }}>
<Toc.Link href="#toc-sizes-usage">Usage</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-props", depth: 2 }}>
<Toc.Link href="#toc-sizes-props">Props</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-events", depth: 2 }}>
<Toc.Link href="#toc-sizes-events">Events</Toc.Link>
</Toc.Item>
<Toc.Item item={{ value: "toc-sizes-styling", depth: 1 }}>
<Toc.Link href="#toc-sizes-styling">Styling</Toc.Link>
</Toc.Item>
</Toc.List>
</Toc.Root>
</Grid>
)
}Recipe
| Variant | Values | Default |
|---|---|---|
variant | line, track | line |
size | sm, md | md |
| Variable | Default | Description |
|---|---|---|
--toc-indent | spacing.2.5 | How far each level is indented, link and track alike |
--toc-link-offset | spacing.4 | The space between the line and a top-level link |
Both take any length or token. Toc.Track measures --toc-indent to draw the line, so changing it moves the line with
the links.