Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Components

Free

Table of Contents for Ark UI

A side list of headings that marks the section on screen and scrolls to the one you pick. For React with Ark UI, you supply the ids, because it doesn't scan the page.

Framework
UI library

An outline of the headings on a long page. The one in view stays marked, and choosing one scrolls there.

Put it next to a doc or any single column of sections. A hierarchy you open and close, with no tie to the scroll position, is a Tree View. In React, you hand Ark UI the heading ids, and it doesn't read them off the page.

Preview width

Installation

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.

Anatomy

Table of Contents anatomy: root, title, indicator, item, link

Usage

import { useCallback, useRef } from "react";
import {
  Toc, TocContent, TocIndicator, TocItem, TocLink, TocList, TocNav,
  TocTitle,
} from "@/components/ui/toc";
interface Item {
  depth: number;
  label: string;
  lines: number;
  value: string;
}

const items: Item[] = [
  {
    depth: 2,
    label: "Introduction",
    lines: 8,
    value: "toc-basic-introduction",
  },
  {
    depth: 2,
    label: "Getting Started",
    lines: 7,
    value: "toc-basic-getting-started",
  },
  {
    depth: 2,
    label: "Installation",
    lines: 6,
    value: "toc-basic-installation",
  },
  { depth: 2, label: "Usage", lines: 9, value: "toc-basic-usage" },
  { depth: 2, label: "Conclusion", lines: 7, value: "toc-basic-conclusion" },
];

const TocDemo = () => {
  const contentRef = useRef<HTMLElement | null>(null);
  const getScrollEl = useCallback(() => contentRef.current, []);

  return (
    <Toc className="max-w-2xl" items={items} scrollEl={getScrollEl}>
      <TocContent className="h-80 pe-2" ref={contentRef}>
        {items.map((item) => (
          <section
            className="not-first:mt-12 flex flex-col gap-3"
            key={item.value}
          >
            <h2
              className="scroll-mt-4 font-heading font-semibold text-lg"
              id={item.value}
            >
              {item.label}
            </h2>
            <div
              className="rounded-md bg-muted"
              style={{ height: `${item.lines * 0.75}rem` }}
            />
          </section>
        ))}
      </TocContent>

      <TocNav scrollToTop>
        <TocTitle>On this page</TocTitle>
        <TocList>
          <TocIndicator />
          {items.map((item) => (
            <TocItem item={item} key={item.value}>
              <TocLink href={`#${item.value}`}>{item.label}</TocLink>
            </TocItem>
          ))}
        </TocList>
      </TocNav>
    </Toc>
  );
};

Demos

Nested

Headings are not all the same level. Each item carries a depth, 2 for a section and 3 for a subsection, and the list indents by that depth. The article renders the matching heading so the outline and the page agree. Use nesting when subsections exist and a flat list would hide which heading belongs under which.

Preview width

Root Provider

Sometimes the active heading is needed outside the nav. useToc creates the state, a line above the article prints the active ids, and TocRootProvider feeds that state to the content and the list. Use this when a header or another control must follow the same heading. If nothing outside the outline cares, the Toc root is less machinery.

Preview width

With Collapsible

A sidebar does not fit on a narrow screen. The outline collapses to a button that shows the current heading and how far through the list you are, with a small Circular Progress for the index. Opening it shows the links. Use this above the article on small layouts, and keep a permanent nav when you have the width.

Preview width

With Hover

The outline can stay out of the way until the pointer is on it. Collapsed, it is a short stack of ticks, and on hover it widens so the labels appear. Use this where a full list would crowd the article. The ticks are not links, so people still need to scroll, or you should offer another way to jump.

Preview width

With Indicator

TocIndicator is a marker that slides next to the active link as you scroll. These sections are very different lengths, so you can watch the marker move instead of hunting for a color change on the text. Use it when the list is long enough that the active link is easy to miss. The link still carries the name, and the marker only makes the position obvious.

Preview width

With Rail

Nested headings can read as a connected outline instead of a list with a sliding bar. Each item draws a short rail, stepping inward as depth increases, and the active branch takes the primary stroke. There is no TocIndicator here. Use the rail for third and fourth level headings, when indentation alone feels like a ragged margin.

Preview width

With Select

On a narrow layout the outline can be a Native Select above the article. Its value follows the heading in view, and choosing an option calls scrollTo from the toc context. Use this when a sidebar will not fit and a collapsible list is more UI than you want. The select is the navigation, so you do not also need the link list on that breakpoint.

Preview width

With Tree View

Deep docs are easier to scan when branches collapse. The outline is a Tree View: a section expands when its child is the active heading, and a leaf scrolls the article to that id. Use this when people should hide the parts they are not reading. One extra level of headings is fine as indented TocItem rows, without a tree.

Preview width

FAQ

How do I build a table of contents from the headings on a page?

Pass items, one per heading: its value is the heading's id, depth is its level and label is its text. The Table of Contents watches those headings as you scroll and marks the one in view as active.

How do I show nested headings in the Table of Contents?

Give each item its depth, 2 for an h2 and 3 for an h3: items are indented by depth, as in the Nested demo. Each TocItem also carries data-depth, so you can style levels differently.

How do I use the Table of Contents in a scrolling container?

Pass scrollEl, a function that returns the element that scrolls, as the default demo does. Without it, the page itself is watched, which is right for a docs page.

How does the Table of Contents decide which heading is active?

It observes the headings with an IntersectionObserver and marks those in view, using rootMargin and threshold to tune when a heading counts as visible. The active link gets data-active, and onActiveChange tells you which ids are active.

How do I add a moving indicator to the Table of Contents?

Add a TocIndicator: it slides next to the active link as you scroll, as the With Indicator demo does. The With Rail demo shows the same idea as a vertical rail.

How do I show the Table of Contents on mobile?

Show the list in a sidebar on wide screens, and on small screens put it in a select or a collapsible, as the With Select and With Collapsible demos do. Clicking a link scrolls smoothly to the heading.

How do I install the Table of Contents in React?

Run the command in the Installation section above: it adds the Table of Contents to your ui folder. It is built on Ark UI on every base and framework. The command uses the shadcn CLI.

API Reference

Toc

<div> data-slot="toc"

PropType
itemsTocItem[]

The TOC items with value (slug/id) and depth (heading level).

activeIds?string[]

The controlled active heading ids.

autoScroll?boolean

Whether to auto-scroll the TOC container so the first active item is visible when active headings change.

Default
true
defaultActiveIds?string[]

The default active heading ids when rendered. Use when you don't need to control the active headings.

dir?"ltr" | "rtl"

The document's text/writing direction.

Type
"ltr" | "rtl"
Default
"ltr"
id?string

The unique identifier of the machine.

ids?object

The ids of the elements in the TOC. Useful for composition.

Type
{ root?: string; title?: string; list?: string; item?: (value: string) → string; link?: (value: string) → string; indicator?: string }
onActiveChange?(details: ActiveChangeDetails) → void

Callback when the active (visible) headings change.

Type
(details: ActiveChangeDetails) → void
Parameters

details – { activeIds: string[]; activeItems: TocItem[] }

Returns
void
rootMargin?string

The root margin for the IntersectionObserver. Controls the effective viewport area for determining active headings.

Default
"-20px 0px -40% 0px"
scrollBehavior?ScrollBehavior

The default scroll behavior used when auto-scrolling the TOC container and when scrolling to a heading (via link click or api.scrollTo). Can be overridden per-call by passing behavior to api.scrollTo.

Default
"smooth"
scrollEl?() → HTMLElement | null

Function that returns the scroll container element to observe within. Defaults to the document/viewport.

Type
() → HTMLElement | null
Returns
HTMLElement | null
threshold?number | number[]

The IntersectionObserver threshold. A value of 0 means the heading is active as soon as even one pixel is visible within the root margin area.

Type
number | number[]
Default
0
asChild?boolean

Use the provided child element as the default rendered element, combining their props and behavior.

className?string

Merged with the part's classes.

CSS variableType
--heightstring

The height of the element.

--leftstring

The left position value.

--topstring

The top position value.

--widthstring

The width of the element.

TocTitle

<h2> data-slot="toc-title"

PropType
asChild?boolean

Use the provided child element as the default rendered element, combining their props and behavior.

className?string

Merged with the part's classes.

TocList

<ul> data-slot="toc-list"

PropType
asChild?boolean

Use the provided child element as the default rendered element, combining their props and behavior.

className?string

Merged with the part's classes.

TocItem

<li> data-slot="toc-item"

PropType
itemobject

The TOC item.

Type
{ value: string; depth: number }
asChild?boolean

Use the provided child element as the default rendered element, combining their props and behavior.

className?string

Merged with the part's classes.

AttributeType
data-active?string

Present when active or pressed.

data-depthstring

The depth of the item.

data-first?string

Present when first is true.

data-last?string

Present when last is true.

data-valuestring

The value of the item.

CSS variableType
--depthstring

The depth value for the Item.

<a> data-slot="toc-link"

PropType
asChild?boolean

Use the provided child element as the default rendered element, combining their props and behavior.

className?string

Merged with the part's classes.

AttributeType
data-active?string

Present when active or pressed.

data-valuestring

The value of the item.

TocIndicator

<div> data-slot="toc-indicator"

PropType
asChild?boolean

Use the provided child element as the default rendered element, combining their props and behavior.

className?string

Merged with the part's classes.

CSS variableType
--heightstring

The measured height, for animating open and closed.

--topstring

TocContent

<article> data-slot="toc-content"

PropType
asChild?boolean

Use the provided child element as the default rendered element, combining their props and behavior.

className?string

Merged with the part's classes.

TocNav

<nav> data-slot="toc-nav"

PropType
placement?"left" | "right"

The side of the content the outline sits on.

Type
"left" | "right"
scrollToTop?boolean

Shows a button under the outline that scrolls back to the top.

Default
false
asChild?boolean

Use the provided child element as the default rendered element, combining their props and behavior.

className?string

Merged with the part's classes.

TocScrollToTop

<button> data-slot="toc-scroll-to-top"

PropType
offset?number

How far the page scrolls before the button shows, in px.

Default
DEFAULT_TOP_OFFSET
className?string

Merged with the part's classes.

TocRootProvider

<div> data-slot="toc"

PropType
valueUseTocReturn

What useToc() returns: the state and API, made outside the component to control it.

asChild?boolean

Use the provided child element as the default rendered element, combining their props and behavior.

className?string

Merged with the part's classes.

CSS variableType
--heightstring

The measured height, for animating open and closed.

--leftstring
--topstring
--widthstring

The measured width, for animating open and closed.

useToc

PropType
itemsTocItem[]

The TOC items with value (slug/id) and depth (heading level).

activeIds?string[]

The controlled active heading ids.

autoScroll?boolean

Whether to auto-scroll the TOC container so the first active item is visible when active headings change.

Default
true
defaultActiveIds?string[]

The default active heading ids when rendered. Use when you don't need to control the active headings.

id?string

The unique identifier of the machine.

ids?object

The ids of the elements in the TOC. Useful for composition.

Type
{ root?: string; title?: string; list?: string; item?: (value: string) → string; link?: (value: string) → string; indicator?: string }
onActiveChange?(details: ActiveChangeDetails) → void

Callback when the active (visible) headings change.

Type
(details: ActiveChangeDetails) → void
Parameters

details – { activeIds: string[]; activeItems: TocItem[] }

Returns
void
rootMargin?string

The root margin for the IntersectionObserver. Controls the effective viewport area for determining active headings.

Default
"-20px 0px -40% 0px"
scrollBehavior?ScrollBehavior

The default scroll behavior used when auto-scrolling the TOC container and when scrolling to a heading (via link click or api.scrollTo). Can be overridden per-call by passing behavior to api.scrollTo.

Default
"smooth"
scrollEl?() → HTMLElement | null

Function that returns the scroll container element to observe within. Defaults to the document/viewport.

Type
() → HTMLElement | null
Returns
HTMLElement | null
threshold?number | number[]

The IntersectionObserver threshold. A value of 0 means the heading is active as soon as even one pixel is visible within the root margin area.

Type
number | number[]
Default
0

TocContext

PropType
activeIdsstring[]

All currently active (visible) heading ids.

activeItemsTocItem[]

The active (visible) TOC items.

getItemState(props: ItemProps) → ItemState

Returns the state of a TOC item.

Parameters

props – { item: TocItem }

Returns
ItemState
itemsTocItem[]

The resolved items list.

scrollTo(value: string, details?: ScrollToDetails) → void

Scrolls to the heading with the given id.

Type
(value: string, details?: ScrollToDetails) → void
Parameters

value – string

details? – { behavior?: ScrollBehavior }

Returns
void
setActiveIds(value: string[]) → void

Manually set the active heading ids.

Parameters

value – string[]

Returns
void

With other components