Skip to content
Kappa
Table of Contents
@dicehub/kappav0.4.2

Table of Contents

Ark-backed section navigation with nested depth, active tracking, and a moving indicator.

<script setup lang="ts">
import { ref } from "vue";
import { TableOfContents, type TocItemData } from "@dicehub/kappa/components/table-of-contents";

const items: TocItemData[] = [
  { value: "installation", depth: 2 },
  { value: "usage", depth: 2 },
  { value: "composition", depth: 2 },
  { value: "examples", depth: 2 },
  { value: "api-reference", depth: 2 },
];
const labels: Record<string, string> = {
  installation: "Installation",
  usage: "Usage",
  composition: "Composition",
  examples: "Examples",
  "api-reference": "API Reference",
};
const activeIds = ref(["usage"]);
const activate = (value: string) => {
  activeIds.value = [value];
};
</script>

<template>
  <TableOfContents.Root :items="items" :active-ids="activeIds">
    <TableOfContents.Nav>
      <TableOfContents.Title>On this page</TableOfContents.Title>
      <TableOfContents.List>
        <TableOfContents.Indicator />
        <TableOfContents.Item v-for="item in items" :key="item.value" :item="item">
          <TableOfContents.Link as-child>
            <button type="button" @click="activate(item.value)">
              {{ labels[item.value] }}
            </button>
          </TableOfContents.Link>
        </TableOfContents.Item>
      </TableOfContents.List>
    </TableOfContents.Nav>
  </TableOfContents.Root>
</template>

Installation

Table of Contents is part of the main Kappa package. Ark UI and Zag own the state machine and visibility tracking.

Barrel

import { TableOfContents } from "@dicehub/kappa";

Granular

import { TableOfContents, type TocItemData } from "@dicehub/kappa/components/table-of-contents";

Usage

Build an items array with heading value ids and numericdepth. Pass the same array to TableOfContents.Root, then compose the Ark-backed parts inside Nav.

Give each TableOfContents.Root and TableOfContents.Nav a distinct id. For a local scroll panel, use scrollEland scroll that element to keep the page position unchanged.

<script setup lang="ts">
import { TableOfContents, type TocItemData } from "@dicehub/kappa/components/table-of-contents";

const items: TocItemData[] = [
  { value: "introduction", depth: 2 },
  { value: "installation", depth: 2 },
  { value: "configuration", depth: 3 },
];
</script>

<template>
  <TableOfContents.Root :items="items">
    <TableOfContents.Content>
      <section v-for="item in items" :key="item.value">
        <component :is="item.depth === 3 ? 'h3' : 'h2'" :id="item.value">{{ item.value }}</component>
      </section>
    </TableOfContents.Content>
    <TableOfContents.Nav>
      <TableOfContents.Title>Guide</TableOfContents.Title>
      <TableOfContents.List>
        <TableOfContents.Item v-for="item in items" :key="item.value" :item="item">
          <TableOfContents.Link :href="`#${item.value}`">{{ item.value }}</TableOfContents.Link>
        </TableOfContents.Item>
      </TableOfContents.List>
    </TableOfContents.Nav>
  </TableOfContents.Root>
</template>

Composition

Typical part hierarchy
TableOfContents.Root
├── TableOfContents.Content
│   └── document headings
└── TableOfContents.Nav
    ├── TableOfContents.Title
    └── TableOfContents.List
        ├── TableOfContents.Indicator
        └── TableOfContents.Item
            └── TableOfContents.Link

Root creates the Ark TOC machine and provides context. Content is an article wrapper, Nav is the semantic navigation region, and Titlelabels it. Item binds one item object; its Link receives the active state and same-page scroll behavior from Ark UI. Indicator tracks the active range.

Use Context for a renderless API readout. Create a machine withuseToc and pass it to RootProvider when state must be created outside the template. Kappa adds rails, depth indentation, focus styles, and semantic light/dark tokens.

Examples

These previews render Link as a button with asChild, so trying an example does not move this documentation page. Use same-page anchors in production.

Basic

Compose a compact labelled Nav, List, Item, and button-backed Link.

<script setup lang="ts">
import { ref } from "vue";
import { TableOfContents, type TocItemData } from "@dicehub/kappa/components/table-of-contents";

const items: TocItemData[] = [
  { value: "installation", depth: 2 },
  { value: "usage", depth: 2 },
  { value: "examples", depth: 2 },
  { value: "api-reference", depth: 2 },
];
const labels: Record<string, string> = {
  installation: "Installation",
  usage: "Usage",
  examples: "Examples",
  "api-reference": "API Reference",
};
const activeIds = ref(["installation"]);
const activate = (value: string) => {
  activeIds.value = [value];
};
</script>

<template>
  <TableOfContents.Root :items="items" :active-ids="activeIds">
    <TableOfContents.Nav>
      <TableOfContents.Title>Sections</TableOfContents.Title>
      <TableOfContents.List>
        <TableOfContents.Item v-for="item in items" :key="item.value" :item="item">
          <TableOfContents.Link as-child>
            <button type="button" @click="activate(item.value)">
              {{ labels[item.value] }}
            </button>
          </TableOfContents.Link>
        </TableOfContents.Item>
      </TableOfContents.List>
    </TableOfContents.Nav>
  </TableOfContents.Root>
</template>

Nested depth

Set each item depth to preserve heading hierarchy and Kappa indentation.

<script setup lang="ts">
import { ref } from "vue";
import { TableOfContents, type TocItemData } from "@dicehub/kappa/components/table-of-contents";

const items: TocItemData[] = [
  { value: "installation", depth: 2 },
  { value: "barrel", depth: 3 },
  { value: "granular", depth: 3 },
  { value: "usage", depth: 2 },
];
const labels: Record<string, string> = {
  installation: "Installation",
  barrel: "Barrel",
  granular: "Granular",
  usage: "Usage",
};
const activeIds = ref(["barrel"]);
const activate = (value: string) => {
  activeIds.value = [value];
};
</script>

<template>
  <TableOfContents.Root :items="items" :active-ids="activeIds">
    <TableOfContents.Nav>
      <TableOfContents.Title>Guide sections</TableOfContents.Title>
      <TableOfContents.List>
        <TableOfContents.Item v-for="item in items" :key="item.value" :item="item">
          <TableOfContents.Link as-child>
            <button type="button" @click="activate(item.value)">
              {{ labels[item.value] }}
            </button>
          </TableOfContents.Link>
        </TableOfContents.Item>
      </TableOfContents.List>
    </TableOfContents.Nav>
  </TableOfContents.Root>
</template>

Active indicator

Add Indicator inside List; Ark UI updates its geometry for the active heading range.

<script setup lang="ts">
import { ref } from "vue";
import { TableOfContents, type TocItemData } from "@dicehub/kappa/components/table-of-contents";

const items: TocItemData[] = [
  { value: "installation", depth: 2 },
  { value: "usage", depth: 2 },
  { value: "composition", depth: 2 },
  { value: "examples", depth: 2 },
];
const labels: Record<string, string> = {
  installation: "Installation",
  usage: "Usage",
  composition: "Composition",
  examples: "Examples",
};
const activeIds = ref(["usage", "composition"]);
const activate = (value: string) => {
  activeIds.value = [value];
};
</script>

<template>
  <TableOfContents.Root
    :items="items"
    :active-ids="activeIds"
  >
    <TableOfContents.Nav>
      <TableOfContents.Title>Visible sections</TableOfContents.Title>
      <TableOfContents.List>
        <TableOfContents.Indicator />
        <TableOfContents.Item v-for="item in items" :key="item.value" :item="item">
          <TableOfContents.Link as-child>
            <button type="button" @click="activate(item.value)">
              {{ labels[item.value] }}
            </button>
          </TableOfContents.Link>
        </TableOfContents.Item>
      </TableOfContents.List>
    </TableOfContents.Nav>
  </TableOfContents.Root>
</template>

Controlled active change

Use activeIds for controlled state and activeChange to reconcile scroll visibility.

<script setup lang="ts">
import { ref } from "vue";
import { Button } from "@dicehub/kappa/components/button";
import {
  TableOfContents,
  type TocActiveChangeDetails,
  type TocItemData,
} from "@dicehub/kappa/components/table-of-contents";

const items: TocItemData[] = [
  { value: "installation", depth: 2 },
  { value: "usage", depth: 2 },
  { value: "api-reference", depth: 2 },
];
const labels: Record<string, string> = {
  installation: "Installation",
  usage: "Usage",
  "api-reference": "API Reference",
};
const activeIds = ref<string[]>(["installation"]);
const changes = ref(0);
const handleActiveChange = ({ activeIds: next }: TocActiveChangeDetails) => {
  activeIds.value = next;
  changes.value += 1;
};
const activateUsage = () => {
  activeIds.value = ["usage"];
};
const activateSection = (value: string) => {
  activeIds.value = [value];
};
</script>

<template>
  <TableOfContents.Root
    :active-ids="activeIds"
    :items="items"
    @active-change="handleActiveChange"
  >
    <TableOfContents.Nav>
      <TableOfContents.Title>Controlled sections</TableOfContents.Title>
      <TableOfContents.List>
        <TableOfContents.Item v-for="item in items" :key="item.value" :item="item">
          <TableOfContents.Link as-child>
            <button type="button" @click="activateSection(item.value)">
              {{ labels[item.value] }}
            </button>
          </TableOfContents.Link>
        </TableOfContents.Item>
      </TableOfContents.List>
    </TableOfContents.Nav>
  </TableOfContents.Root>
  <Button size="sm" variant="outline" @click="activateUsage">
    Activate usage
  </Button>
  <output role="status">
    Active: {{ activeIds.join(", ") }} · Changes: {{ changes }}
  </output>
</template>

Scroll tracking

Pass scrollEl for a real scroll container. Ark observes its headings while the demo buttons avoid page navigation.

<script setup lang="ts">
import { ref } from "vue";
import { TableOfContents, type TocItemData } from "@dicehub/kappa/components/table-of-contents";

const items: TocItemData[] = [
  { value: "overview", depth: 2 },
  { value: "api", depth: 2 },
];
const scrollRoot = ref<HTMLElement | null>(null);
const scrollEl = () => scrollRoot.value;
const scrollToItem = (value: string) => {
  const container = scrollRoot.value;
  const heading = container?.querySelector<HTMLElement>(`#${CSS.escape(value)}`);
  if (!container || !heading) return;
  container.scrollTo({
    top: container.scrollTop + heading.getBoundingClientRect().top -
      container.getBoundingClientRect().top - container.clientTop -
      Number.parseFloat(getComputedStyle(container).paddingTop),
    behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth",
  });
};
</script>

<template>
  <TableOfContents.Root id="guide-toc" :items="items" :scroll-el="scrollEl" :auto-scroll="false">
    <TableOfContents.Content>
      <div ref="scrollRoot" class="scroll-container">
        <h2 id="overview">Overview</h2>
        <h2 id="api">API</h2>
      </div>
    </TableOfContents.Content>
    <TableOfContents.Nav id="guide-toc-nav">
      <TableOfContents.Title>Guide sections</TableOfContents.Title>
      <TableOfContents.List>
        <TableOfContents.Indicator />
        <TableOfContents.Item v-for="item in items" :key="item.value" :item="item">
          <TableOfContents.Link as-child>
            <button type="button" @click="scrollToItem(item.value)">
              {{ item.value }}
            </button>
          </TableOfContents.Link>
        </TableOfContents.Item>
      </TableOfContents.List>
    </TableOfContents.Nav>
  </TableOfContents.Root>
</template>

Accessibility

  • Give Nav a visible Title; Ark uses it to provide the navigation name.
  • Use real heading ids in items; the observed heading and item value must match.
  • Keep links as links. Ark preserves same-page navigation, focus, and scroll behavior.
  • Active state is exposed with data-active and aria-current="location".
  • Focus-visible styles, forced-colors styles, and reduced-motion behavior are included.

Keyboard support

KeyBehavior
TabMoves through links using the browser's normal navigation order.
EnterActivates a link and scrolls to its same-page heading.
Shift + TabMoves backward through TOC links.

API Reference

TableOfContents.Root

PropTypeDefaultDescription
itemsTocItemData[]requiredHeading ids and depths. Each value must match a heading id in the observed document.
activeIds / defaultActiveIdsstring[][]Controlled or initial active heading ids.
scrollEl() => HTMLElement | nullviewportReturns the scroll container used by IntersectionObserver and link scrolling.
rootMarginstring"-20px 0% -40% 0%"IntersectionObserver root margin for active heading detection.
thresholdnumber | number[]0IntersectionObserver threshold.
autoScrollbooleantrueKeeps the first active TOC item visible in the list.
scrollBehaviorScrollBehavior"smooth"Default behavior for link scrolling and active-item auto-scroll.

Parts

PartElementDescription
TableOfContents.RootdivCreates the Ark TOC machine and provides context.
TableOfContents.ContentarticleContent region for the TOC composition.
TableOfContents.NavnavSemantic navigation region labelled by Title.
TableOfContents.Titleh2Required visible heading used to label Nav.
TableOfContents.ListulList container for TOC items and Indicator.
TableOfContents.ItemliBinds one { value, depth } item to the Ark machine.
TableOfContents.LinkaScrolls to a same-page heading and exposes active state.
TableOfContents.IndicatordivTracks the first-to-last active item geometry.
TableOfContents.ContextrenderlessExposes the Ark TOC API to a scoped slot.
TableOfContents.RootProviderdivUses a TOC API created with useToc outside the template.

Events

EventPayloadDescription
activeChange{ activeIds: string[]; activeItems: TocItemData[] }Emitted when observed heading visibility changes.

Data attributes

AttributeValueDescription
data-activepresentMarks items and links whose heading is active.
data-first / data-lastpresentMarks the first and last active item.
data-depthnumberExposes the item heading depth.
data-valuestringExposes the heading id on Item and Link.

Exports

ExportDescription
TableOfContentsCompound namespace with Root, Content, Nav, Title, List, Item, Link, Indicator, Context, and RootProvider.
TableOfContentsRoot … TableOfContentsRootProviderNamed component exports for granular composition.
TableOfContentsRootProps and part prop typesPublic Ark-aligned Vue prop contracts.
TocItemData / TocActiveChangeDetailsArk UI TOC data and event contracts.
useToc / useTocContext / tocAnatomyArk UI behavior hooks and anatomy exports.