Table of Contents
@cloudflare/kumo
import { TableOfContents } from "@cloudflare/kumo";

export function TableOfContentsBasicDemo() {
  return (
    <DemoWrapper>
      <TableOfContents>
        <TableOfContents.Title>On this page</TableOfContents.Title>
        <TableOfContents.List>
          {headings.map((heading) => (
            <TableOfContents.Item
              key={heading.text}
              active={heading.text === "Usage"}
              className="cursor-pointer"
            >
              {heading.text}
            </TableOfContents.Item>
          ))}
        </TableOfContents.List>
      </TableOfContents>
    </DemoWrapper>
  );
}

Installation

Barrel

import { TableOfContents } from "@cloudflare/kumo";

Granular

import { TableOfContents } from "@cloudflare/kumo/components/table-of-contents";

Usage

import { TableOfContents } from "@cloudflare/kumo";

export default function Example() {
  return (
    <TableOfContents>
      <TableOfContents.Title>On this page</TableOfContents.Title>
      <TableOfContents.List>
        <TableOfContents.Item href="#intro" active>
          Introduction
        </TableOfContents.Item>
        <TableOfContents.Item href="#api">API Reference</TableOfContents.Item>
      </TableOfContents.List>
    </TableOfContents>
  );
}

This component is purely presentational. All interaction logic — scroll tracking, IntersectionObserver, active state management — is left to the consumer.

Examples

Interactive

Click an item to set it as active. The consumer controls state via active and onClick.

import { useState } from "react";
import { TableOfContents } from "@cloudflare/kumo";

export function TableOfContentsInteractiveDemo() {
  const [active, setActive] = useState("Introduction");

  return (
    <DemoWrapper>
      <TableOfContents>
        <TableOfContents.Title>On this page</TableOfContents.Title>
        <TableOfContents.List>
          {headings.map((heading) => (
            <TableOfContents.Item
              key={heading.text}
              active={heading.text === active}
              onClick={() => setActive(heading.text)}
              className="cursor-pointer"
            >
              {heading.text}
            </TableOfContents.Item>
          ))}
        </TableOfContents.List>
      </TableOfContents>
    </DemoWrapper>
  );
}

No active item

When no item has active set, all items show the default subtle text style with a hover indicator.

import { TableOfContents } from "@cloudflare/kumo";

export function TableOfContentsNoActiveDemo() {
  return (
    <DemoWrapper>
      <TableOfContents>
        <TableOfContents.Title>On this page</TableOfContents.Title>
        <TableOfContents.List>
          {headings.map((heading) => (
            <TableOfContents.Item key={heading.text} className="cursor-pointer">
              {heading.text}
            </TableOfContents.Item>
          ))}
        </TableOfContents.List>
      </TableOfContents>
    </DemoWrapper>
  );
}

Groups

Use TableOfContents.Group to organize items into labeled sections with indented children. Groups support two modes: pass an href to make the group label a clickable link (like “Examples” and “API” below), or omit it for a plain non-interactive title (like “Getting Started”).

import { TableOfContents } from "@cloudflare/kumo";

/** Shows both group modes: clickable group labels (with `href`) and plain title labels (without `href`). */
export function TableOfContentsGroupDemo() {
  return (
    <DemoWrapper>
      <TableOfContents>
        <TableOfContents.Title>On this page</TableOfContents.Title>
        <TableOfContents.List>
          <TableOfContents.Item active className="cursor-pointer">
            Overview
          </TableOfContents.Item>
          <TableOfContents.Group label="Examples" href="#examples-demo">
            <TableOfContents.Item className="cursor-pointer">
              Basic example
            </TableOfContents.Item>
            <TableOfContents.Item className="cursor-pointer">
              Advanced example
            </TableOfContents.Item>
          </TableOfContents.Group>
          <TableOfContents.Group label="Getting Started">
            <TableOfContents.Item className="cursor-pointer">
              Installation
            </TableOfContents.Item>
            <TableOfContents.Item className="cursor-pointer">
              Configuration
            </TableOfContents.Item>
          </TableOfContents.Group>
          <TableOfContents.Group label="API" href="#api-demo">
            <TableOfContents.Item className="cursor-pointer">
              Props
            </TableOfContents.Item>
            <TableOfContents.Item className="cursor-pointer">
              Events
            </TableOfContents.Item>
          </TableOfContents.Group>
        </TableOfContents.List>
      </TableOfContents>
    </DemoWrapper>
  );
}

Without title

The title sub-component is optional — use TableOfContents.List directly if you don’t need a heading.

import { TableOfContents } from "@cloudflare/kumo";

export function TableOfContentsWithoutTitleDemo() {
  return (
    <DemoWrapper>
      <TableOfContents>
        <TableOfContents.List>
          {headings.slice(0, 3).map((heading) => (
            <TableOfContents.Item
              key={heading.text}
              active={heading.text === "Introduction"}
              className="cursor-pointer"
            >
              {heading.text}
            </TableOfContents.Item>
          ))}
        </TableOfContents.List>
      </TableOfContents>
    </DemoWrapper>
  );
}

Custom element

Use the render prop to swap the default anchor for a button, router link, or any element.

import { useState } from "react";
import { TableOfContents } from "@cloudflare/kumo";

/** Demonstrates using the `render` prop with a custom link component. */
export function TableOfContentsRenderPropDemo() {
  const [clicked, setClicked] = useState<string | null>(null);

  return (
    <DemoWrapper>
      <div className="space-y-3">
        <TableOfContents>
          <TableOfContents.List>
            {["Introduction", "Installation", "Usage"].map((text) => (
              <TableOfContents.Item
                key={text}
                render={<button type="button" />}
                onClick={() => setClicked(text)}
                active={text === "Introduction"}
              >
                {text}
              </TableOfContents.Item>
            ))}
          </TableOfContents.List>
        </TableOfContents>
        {clicked && (
          <p className="text-xs text-kumo-subtle">Clicked: {clicked}</p>
        )}
      </div>
    </DemoWrapper>
  );
}

React Router

<TableOfContents.Item render={<Link to="/intro" />} active>
  Introduction
</TableOfContents.Item>

Next.js

import Link from "next/link";

<TableOfContents.Item render={<Link href="/intro" />} active>
  Introduction
</TableOfContents.Item>;

Button (no navigation)

<TableOfContents.Item render={<button type="button" />} onClick={handleClick}>
  Introduction
</TableOfContents.Item>

API Reference

TableOfContents

Root nav container with a default aria-label of “Table of contents”.

PropTypeDefault
childrenReactNode-
classNamestring-
idstring-
langstring-
titlestring-

TableOfContents.Title

Optional uppercase heading displayed above the list (renders a <p>).

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

TableOfContents.List

List container with a left border rail.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

TableOfContents.Item

Individual navigation link. Set active for the current section. Use the render prop to swap the anchor for a router link or button.

PropTypeDefault
activeboolean-

TableOfContents.Group

Groups items under a labeled section with indented children. Pass href to make the label a clickable link, or omit for a plain title.

PropTypeDefault
label*string-
hrefstring-
activeboolean-