{"slug":"toc","title":"Table of Contents","description":"Using the table of contents machine in your project.","contentType":"component","content":"A table of contents tracks which headings are visible on the page using\n`IntersectionObserver` and highlights the corresponding TOC items. Commonly used\nin documentation sidebars.\n\n## Resources\n\n\n[Latest version: v](https://www.npmjs.com/package/@zag-js/toc)\n[Logic Visualizer](https://zag-visualizer.vercel.app/toc)\n[Source Code](https://github.com/chakra-ui/zag/tree/main/packages/machines/toc)\n\n**Features**\n\n- Tracks visible headings with `IntersectionObserver`\n- Supports multiple active headings simultaneously\n- Auto-scrolls the TOC sidebar to keep the active item visible\n- Animated indicator for the active item\n- Custom scroll container support\n- Controlled and uncontrolled active state\n\n## Installation\n\nInstall the table of contents package:\n\n**React**\n\n```bash\nnpm install @zag-js/toc @zag-js/react\n# or\nyarn add @zag-js/toc @zag-js/react\n```\n\n**Solid**\n\n```bash\nnpm install @zag-js/toc @zag-js/solid\n# or\nyarn add @zag-js/toc @zag-js/solid\n```\n\n**Vue**\n\n```bash\nnpm install @zag-js/toc @zag-js/vue\n# or\nyarn add @zag-js/toc @zag-js/vue\n```\n\n**Svelte**\n\n```bash\nnpm install @zag-js/toc @zag-js/svelte\n# or\nyarn add @zag-js/toc @zag-js/svelte\n```\n\n## Anatomy\n\nCheck the table of contents anatomy and part names.\n\n> Each part includes a `data-part` attribute to help identify them in the DOM.\n\n\n\n## Usage\n\nImport the table of contents package:\n\n```tsx\nimport * as toc from \"@zag-js/toc\"\n```\n\nThe table of contents package exports two key functions:\n\n- `machine` - State machine logic.\n- `connect` - Maps machine state to JSX props and event handlers.\n\nYou'll need to provide `items` — an array of `{ value, depth }` objects\nrepresenting the headings on the page.\n\n**React**\n\n```tsx\nimport * as toc from \"@zag-js/toc\"\nimport { useMachine, normalizeProps } from \"@zag-js/react\"\nimport { useId } from \"react\"\n\nconst items = [\n  { value: \"introduction\", depth: 2 },\n  { value: \"getting-started\", depth: 2 },\n  { value: \"installation\", depth: 3 },\n  { value: \"usage\", depth: 2 },\n]\n\nexport function TableOfContents() {\n  const service = useMachine(toc.machine, { id: useId(), items })\n\n  const api = toc.connect(service, normalizeProps)\n\n  return (\n    <nav {...api.getRootProps()}>\n      <h5 {...api.getTitleProps()}>On this page</h5>\n      <ul {...api.getListProps()}>\n        {api.items.map((item) => (\n          <li key={item.value} {...api.getItemProps({ item })}>\n            <a href={`#${item.value}`} {...api.getLinkProps({ item })}>\n              {item.value}\n            </a>\n          </li>\n        ))}\n      </ul>\n    </nav>\n  )\n}\n```\n\n**Solid**\n\n```jsx\nimport * as toc from \"@zag-js/toc\"\nimport { normalizeProps, useMachine } from \"@zag-js/solid\"\nimport { createMemo, createUniqueId, For } from \"solid-js\"\n\nconst items = [\n  { value: \"introduction\", depth: 2 },\n  { value: \"getting-started\", depth: 2 },\n  { value: \"installation\", depth: 3 },\n  { value: \"usage\", depth: 2 },\n]\n\nexport function TableOfContents() {\n  const service = useMachine(toc.machine, { id: createUniqueId(), items })\n\n  const api = createMemo(() => toc.connect(service, normalizeProps))\n\n  return (\n    <nav {...api().getRootProps()}>\n      <h5 {...api().getTitleProps()}>On this page</h5>\n      <ul {...api().getListProps()}>\n        <For each={api().items}>\n          {(item) => (\n            <li {...api().getItemProps({ item })}>\n              <a href={`#${item.value}`} {...api().getLinkProps({ item })}>\n                {item.value}\n              </a>\n            </li>\n          )}\n        </For>\n      </ul>\n    </nav>\n  )\n}\n```\n\n**Vue**\n\n```html\n<script setup>\n  import * as toc from \"@zag-js/toc\"\n  import { normalizeProps, useMachine } from \"@zag-js/vue\"\n  import { computed } from \"vue\"\n\n  const items = [\n    { value: \"introduction\", depth: 2 },\n    { value: \"getting-started\", depth: 2 },\n    { value: \"installation\", depth: 3 },\n    { value: \"usage\", depth: 2 },\n  ]\n\n  const service = useMachine(toc.machine, { id: \"1\", items })\n  const api = computed(() => toc.connect(service, normalizeProps))\n</script>\n\n<template>\n  <nav v-bind=\"api.getRootProps()\">\n    <h5 v-bind=\"api.getTitleProps()\">On this page</h5>\n    <ul v-bind=\"api.getListProps()\">\n      <li\n        v-for=\"item in api.items\"\n        :key=\"item.value\"\n        v-bind=\"api.getItemProps({ item })\"\n      >\n        <a :href=\"`#${item.value}`\" v-bind=\"api.getLinkProps({ item })\">\n          {{ item.value }}\n        </a>\n      </li>\n    </ul>\n  </nav>\n</template>\n```\n\n**Svelte**\n\n```svelte\n<script lang=\"ts\">\n  import * as toc from \"@zag-js/toc\"\n  import { useMachine, normalizeProps } from \"@zag-js/svelte\"\n\n  const items = [\n    { value: \"introduction\", depth: 2 },\n    { value: \"getting-started\", depth: 2 },\n    { value: \"installation\", depth: 3 },\n    { value: \"usage\", depth: 2 },\n  ]\n\n  const id = $props.id()\n  const service = useMachine(toc.machine, { id, items })\n  const api = $derived(toc.connect(service, normalizeProps))\n</script>\n\n<nav {...api.getRootProps()}>\n  <h5 {...api.getTitleProps()}>On this page</h5>\n  <ul {...api.getListProps()}>\n    {#each api.items as item}\n      <li {...api.getItemProps({ item })}>\n        <a href={`#${item.value}`} {...api.getLinkProps({ item })}>\n          {item.value}\n        </a>\n      </li>\n    {/each}\n  </ul>\n</nav>\n```\n\n### Adding an indicator\n\nUse `getIndicatorProps()` to render an animated bar that follows the active\nitem. The indicator position is exposed as CSS variables on the root element.\n\n```tsx {5}\n<nav {...api.getRootProps()}>\n  <h5 {...api.getTitleProps()}>On this page</h5>\n  <ul {...api.getListProps()}>\n    <div {...api.getIndicatorProps()} />\n    {api.items.map((item) => (\n      <li key={item.value} {...api.getItemProps({ item })}>\n        <a href={`#${item.value}`} {...api.getLinkProps({ item })}>\n          {item.value}\n        </a>\n      </li>\n    ))}\n  </ul>\n</nav>\n```\n\n### Configuring the observer\n\nControl how headings are detected with `rootMargin` and `threshold`.\n\n```tsx {2-3}\nconst service = useMachine(toc.machine, {\n  rootMargin: \"-20px 0% -40% 0%\",\n  threshold: 0,\n})\n```\n\n### Custom scroll container\n\nBy default, the machine observes the document viewport. Pass `scrollEl` to\nobserve headings within a specific scrollable container.\n\n```tsx {2}\nconst service = useMachine(toc.machine, {\n  scrollEl: () => document.getElementById(\"content\"),\n})\n```\n\n### Auto-scrolling the TOC\n\nWhen the TOC sidebar is scrollable, the machine auto-scrolls to keep the first\nactive item visible. Disable this with `autoScroll`.\n\n```tsx {2}\nconst service = useMachine(toc.machine, {\n  autoScroll: false,\n})\n```\n\n### Listening for active changes\n\nUse `onActiveChange` to respond when visible headings change. The callback\nreceives both `activeIds` and `activeItems`.\n\n```tsx {2-5}\nconst service = useMachine(toc.machine, {\n  onActiveChange(details) {\n    // details => { activeIds: string[], activeItems: TocItem[] }\n    console.log(details.activeIds)\n  },\n})\n```\n\n### Controlled active state\n\nUse `activeIds` and `onActiveChange` for controlled usage.\n\n```tsx\nconst service = useMachine(toc.machine, {\n  activeIds,\n  onActiveChange(details) {\n    setActiveIds(details.activeIds)\n  },\n})\n```\n\n### Getting item state\n\nUse `getItemState` to check if an item is active, first, or last.\n\n```tsx\nconst itemState = api.getItemState({ item })\n// itemState => { active: boolean, first: boolean, last: boolean, depth: number }\n```\n\n## Styling guide\n\nEach part includes a `data-part` attribute you can target in CSS.\n\n```css\n[data-scope=\"toc\"][data-part=\"root\"] {\n  /* styles for the root container */\n}\n\n[data-scope=\"toc\"][data-part=\"link\"] {\n  /* styles for each link */\n}\n\n[data-scope=\"toc\"][data-part=\"link\"][data-active] {\n  /* styles for the active link */\n}\n```\n\n### Active state\n\nWhen a heading is visible, `data-active` is set on the corresponding item and\nlink.\n\n```css\n[data-scope=\"toc\"][data-part=\"item\"][data-active] {\n  /* active item */\n}\n\n[data-scope=\"toc\"][data-part=\"link\"][data-active] {\n  /* active link */\n}\n```\n\n### Depth-based indentation\n\nUse the `--depth` CSS variable to indent items based on heading level.\n\n```css\n[data-scope=\"toc\"][data-part=\"item\"] {\n  padding-inline-start: calc(var(--depth) * 0.75rem - 1rem);\n}\n```\n\n### Indicator styling\n\nThe indicator position is exposed as CSS variables on the root element:\n`--top`, `--left`, `--width`, `--height`.\n\n```css\n[data-scope=\"toc\"][data-part=\"indicator\"] {\n  width: 2px;\n  background-color: blue;\n  position: absolute;\n  inset-inline-start: 0;\n  top: var(--top);\n  height: var(--height);\n  transition: top 0.15s, height 0.15s;\n}\n```\n\n## Methods and Properties\n\n### Machine Context\n\nThe table of contents machine exposes the following context properties:\n\n**`ids`**\nType: `ElementIds | undefined`\nDescription: The ids of the elements in the TOC. Useful for composition.\n\n**`items`**\nType: `TocItem[]`\nDescription: The TOC items with `value` (slug/id) and `depth` (heading level).\n\n**`rootMargin`**\nType: `string | undefined`\nDescription: The root margin for the IntersectionObserver.\nControls the effective viewport area for determining active headings.\n\n**`threshold`**\nType: `number | number[] | undefined`\nDescription: The IntersectionObserver threshold. A value of `0` means the heading is\nactive as soon as even one pixel is visible within the root margin area.\n\n**`scrollEl`**\nType: `(() => HTMLElement | null) | undefined`\nDescription: Function that returns the scroll container element to observe within.\nDefaults to the document/viewport.\n\n**`autoScroll`**\nType: `boolean | undefined`\nDescription: Whether to auto-scroll the TOC container so the first active item\nis visible when active headings change.\n\n**`scrollBehavior`**\nType: `ScrollBehavior | undefined`\nDescription: The default scroll behavior used when auto-scrolling the TOC container\nand when scrolling to a heading (via link click or `api.scrollTo`).\nCan be overridden per-call by passing `behavior` to `api.scrollTo`.\n\n**`onActiveChange`**\nType: `((details: ActiveChangeDetails) => void) | undefined`\nDescription: Callback when the active (visible) headings change.\n\n**`activeIds`**\nType: `string[] | undefined`\nDescription: The controlled active heading ids.\n\n**`defaultActiveIds`**\nType: `string[] | undefined`\nDescription: The default active heading ids when rendered.\nUse when you don't need to control the active headings.\n\n**`dir`**\nType: `\"ltr\" | \"rtl\" | undefined`\nDescription: The document's text/writing direction.\n\n**`id`**\nType: `string`\nDescription: The unique identifier of the machine.\n\n**`getRootNode`**\nType: `(() => ShadowRoot | Document | Node) | undefined`\nDescription: A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.\n\n### Machine API\n\nThe table of contents `api` exposes the following methods:\n\n**`activeIds`**\nType: `string[]`\nDescription: All currently active (visible) heading ids\n\n**`activeItems`**\nType: `TocItem[]`\nDescription: The active (visible) TOC items\n\n**`items`**\nType: `TocItem[]`\nDescription: The resolved items list\n\n**`setActiveIds`**\nType: `(value: string[]) => void`\nDescription: Manually set the active heading ids\n\n**`scrollTo`**\nType: `(value: string, details?: ScrollToDetails | undefined) => void`\nDescription: Scrolls to the heading with the given id.\n\n**`getItemState`**\nType: `(props: ItemProps) => ItemState`\nDescription: Returns the state of a TOC item\n\n### Data Attributes\n\n**`Item`**\n\n**`data-scope`**: toc\n**`data-part`**: item\n**`data-value`**: The value of the item\n**`data-depth`**: The depth of the item\n**`data-active`**: Present when active or pressed\n**`data-first`**: \n**`data-last`**: \n\n**`Link`**\n\n**`data-scope`**: toc\n**`data-part`**: link\n**`data-value`**: The value of the item\n**`data-active`**: Present when active or pressed\n\n### CSS Variables\n\n<CssVarTable name=\"toc\" />","package":"@zag-js/toc","editUrl":"https://github.com/chakra-ui/zag/edit/main/website/data/components/toc.mdx"}