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.

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

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="Getting Started">
            <TableOfContents.Item className="cursor-pointer">
              Installation
            </TableOfContents.Item>
            <TableOfContents.Item className="cursor-pointer">
              Configuration
            </TableOfContents.Item>
          </TableOfContents.Group>
          <TableOfContents.Group label="API">
            <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
<TableOfContents.Item render={<Link href="/intro" />} active>
  Introduction
</TableOfContents.Item>

// Button (no navigation)
<TableOfContents.Item render={<button type="button" />} onClick={handleClick}>
  Introduction
</TableOfContents.Item>

With Next.js

import Link from "next/link";

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

API Reference

PropTypeDefault
childrenReactNode-
classNamestring-
idstring-
langstring-
titlestring-