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.

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.

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.

Recipe

VariantValuesDefault
variantline, trackline
sizesm, mdmd
VariableDefaultDescription
--toc-indentspacing.2.5How far each level is indented, link and track alike
--toc-link-offsetspacing.4The 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.