<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
TableOfContents.Root
├── TableOfContents.Content
│ └── document headings
└── TableOfContents.Nav
├── TableOfContents.Title
└── TableOfContents.List
├── TableOfContents.Indicator
└── TableOfContents.Item
└── TableOfContents.LinkRoot 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
Nava visibleTitle; Ark uses it to provide the navigation name. - Use real heading ids in
items; the observed heading and itemvaluemust match. - Keep links as links. Ark preserves same-page navigation, focus, and scroll behavior.
- Active state is exposed with
data-activeandaria-current="location". - Focus-visible styles, forced-colors styles, and reduced-motion behavior are included.
Keyboard support
| Key | Behavior |
|---|---|
Tab | Moves through links using the browser's normal navigation order. |
Enter | Activates a link and scrolls to its same-page heading. |
Shift + Tab | Moves backward through TOC links. |
API Reference
TableOfContents.Root
| Prop | Type | Default | Description |
|---|---|---|---|
items | TocItemData[] | required | Heading ids and depths. Each value must match a heading id in the observed document. |
activeIds / defaultActiveIds | string[] | [] | Controlled or initial active heading ids. |
scrollEl | () => HTMLElement | null | viewport | Returns the scroll container used by IntersectionObserver and link scrolling. |
rootMargin | string | "-20px 0% -40% 0%" | IntersectionObserver root margin for active heading detection. |
threshold | number | number[] | 0 | IntersectionObserver threshold. |
autoScroll | boolean | true | Keeps the first active TOC item visible in the list. |
scrollBehavior | ScrollBehavior | "smooth" | Default behavior for link scrolling and active-item auto-scroll. |
Parts
| Part | Element | Description |
|---|---|---|
TableOfContents.Root | div | Creates the Ark TOC machine and provides context. |
TableOfContents.Content | article | Content region for the TOC composition. |
TableOfContents.Nav | nav | Semantic navigation region labelled by Title. |
TableOfContents.Title | h2 | Required visible heading used to label Nav. |
TableOfContents.List | ul | List container for TOC items and Indicator. |
TableOfContents.Item | li | Binds one { value, depth } item to the Ark machine. |
TableOfContents.Link | a | Scrolls to a same-page heading and exposes active state. |
TableOfContents.Indicator | div | Tracks the first-to-last active item geometry. |
TableOfContents.Context | renderless | Exposes the Ark TOC API to a scoped slot. |
TableOfContents.RootProvider | div | Uses a TOC API created with useToc outside the template. |
Events
| Event | Payload | Description |
|---|---|---|
activeChange | { activeIds: string[]; activeItems: TocItemData[] } | Emitted when observed heading visibility changes. |
Data attributes
| Attribute | Value | Description |
|---|---|---|
data-active | present | Marks items and links whose heading is active. |
data-first / data-last | present | Marks the first and last active item. |
data-depth | number | Exposes the item heading depth. |
data-value | string | Exposes the heading id on Item and Link. |
Exports
| Export | Description |
|---|---|
TableOfContents | Compound namespace with Root, Content, Nav, Title, List, Item, Link, Indicator, Context, and RootProvider. |
TableOfContentsRoot … TableOfContentsRootProvider | Named component exports for granular composition. |
TableOfContentsRootProps and part prop types | Public Ark-aligned Vue prop contracts. |
TocItemData / TocActiveChangeDetails | Ark UI TOC data and event contracts. |
useToc / useTocContext / tocAnatomy | Ark UI behavior hooks and anatomy exports. |