import { TableOfContents } from "@photon-ai/kumo-solid";
export function TableOfContentsBasicDemo() {
return (
<DemoWrapper>
<TableOfContents>
<TableOfContents.Title>On this page</TableOfContents.Title>
<TableOfContents.List>
{headings.map((heading) => (
<TableOfContents.Item
active={heading.text === "Usage"}
className="cursor-pointer"
>
{heading.text}
</TableOfContents.Item>
))}
</TableOfContents.List>
</TableOfContents>
</DemoWrapper>
);
}Installation
Barrel
import { TableOfContents } from "@photon-ai/kumo-solid";Granular
import { TableOfContents } from "@photon-ai/kumo-solid/components/table-of-contents";Usage
import { TableOfContents } from "@photon-ai/kumo-solid";
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 — you control active on each item.
For automatic scroll tracking, pair it with the useTableOfContentsActiveId
hook below.
Scroll tracking
useTableOfContentsActiveId derives the active section from scroll position:
pass the section ids in document order and it returns the id of the topmost
section in view, plus a selectSection action that pins a clicked section
until the smooth scroll settles (so short sections stay highlighted after a
jump). Hash deep-links (#usage) are handled automatically. The hook is
SSR-safe — all DOM access happens in effects.
import { TableOfContents, useTableOfContentsActiveId } from "@photon-ai/kumo-solid";
export default function Example({ headings }) {
const { activeId, selectSection } = useTableOfContentsActiveId({
ids: headings.map((h) => h.slug),
offset: 64, // fixed header height in px
});
return (
<TableOfContents>
<TableOfContents.Title>On this page</TableOfContents.Title>
<TableOfContents.List>
{headings.map((h) => (
<TableOfContents.Item
href={`#${h.slug}`}
active={activeId === h.slug}
onClick={() => selectSection(h.slug)}
>
{h.text}
</TableOfContents.Item>
))}
</TableOfContents.List>
</TableOfContents>
);
}Pass a root element to track a custom scroll container instead of the
viewport (and trackHash: false when hash navigation doesn’t apply):
Overview
Scrollable placeholder copy for the Overview section. Scrollable placeholder copy for the Overview section. Scrollable placeholder copy for the Overview section. Scrollable placeholder copy for the Overview section. Scrollable placeholder copy for the Overview section. Scrollable placeholder copy for the Overview section.
Installation
Scrollable placeholder copy for the Installation section. Scrollable placeholder copy for the Installation section. Scrollable placeholder copy for the Installation section. Scrollable placeholder copy for the Installation section. Scrollable placeholder copy for the Installation section. Scrollable placeholder copy for the Installation section.
Usage
Scrollable placeholder copy for the Usage section. Scrollable placeholder copy for the Usage section. Scrollable placeholder copy for the Usage section. Scrollable placeholder copy for the Usage section. Scrollable placeholder copy for the Usage section. Scrollable placeholder copy for the Usage section.
API
Scrollable placeholder copy for the API section. Scrollable placeholder copy for the API section. Scrollable placeholder copy for the API section. Scrollable placeholder copy for the API section. Scrollable placeholder copy for the API section. Scrollable placeholder copy for the API section.
import { createSignal } from "solid-js";
import { TableOfContents, useTableOfContentsActiveId } from "@photon-ai/kumo-solid";
/**
* Live scroll tracking via `useTableOfContentsActiveId`, scoped to a custom
* scroll container through the `root` option. Scroll the content — the active
* item follows; click an item to jump.
*/
export function TableOfContentsScrollspyDemo() {
const [root, setRoot] = createSignal<HTMLDivElement | null>(null);
const { activeId, selectSection } = useTableOfContentsActiveId({
ids: scrollspySections.map((s) => s.id),
root,
trackHash: false,
});
return (
<div class="flex w-full max-w-xl gap-6">
<div class="min-w-40">
<TableOfContents>
<TableOfContents.Title>On this page</TableOfContents.Title>
<TableOfContents.List>
{scrollspySections.map((section) => (
<TableOfContents.Item
render={(renderProps) => (
<button {...renderProps} type="button" />
)}
active={activeId() === section.id}
onClick={() => {
selectSection(section.id);
root()
?.querySelector(`#${section.id}`)
?.scrollIntoView({ behavior: "smooth", block: "start" });
}}
>
{section.title}
</TableOfContents.Item>
))}
</TableOfContents.List>
</TableOfContents>
</div>
<div
ref={setRoot}
class="h-64 flex-1 overflow-y-auto rounded-lg border border-kumo-hairline p-4"
>
{scrollspySections.map((section) => (
<section>
<h4 id={section.id} class="mb-2 scroll-mt-2 text-sm font-semibold">
{section.title}
</h4>
<p class="mb-6 text-sm text-kumo-subtle">
{Array.from(
{ length: 6 },
() =>
`Scrollable placeholder copy for the ${section.title} section. `,
).join("")}
</p>
</section>
))}
<div class="h-40" />
</div>
</div>
);
}Options
ids — section anchor ids, in document order (required). offset — px from
the top of the viewport/root to the activation line, typically your fixed
header height (default 0). root — custom scroll container (default:
viewport). trackHash — select the section from location.hash on load and
hashchange (default true).
Examples
Interactive
Click an item to set it as active. The consumer controls state via active
and onClick.
import { createSignal } from "solid-js";
import { TableOfContents } from "@photon-ai/kumo-solid";
export function TableOfContentsInteractiveDemo() {
const [active, setActive] = createSignal("Introduction");
return (
<DemoWrapper>
<TableOfContents>
<TableOfContents.Title>On this page</TableOfContents.Title>
<TableOfContents.List>
{headings.map((heading) => (
<TableOfContents.Item
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 "@photon-ai/kumo-solid";
export function TableOfContentsNoActiveDemo() {
return (
<DemoWrapper>
<TableOfContents>
<TableOfContents.Title>On this page</TableOfContents.Title>
<TableOfContents.List>
{headings.map((heading) => (
<TableOfContents.Item className="cursor-pointer">
{heading.text}
</TableOfContents.Item>
))}
</TableOfContents.List>
</TableOfContents>
</DemoWrapper>
);
}Groups
Use TableOfContents.Group to organize items into labeled sections with
indented children. Groups support two modes: pass an href to make the group
label a clickable link (like “Examples” and “API” below), or omit it for a
plain non-interactive title (like “Getting Started”).
import { TableOfContents } from "@photon-ai/kumo-solid";
/** Shows both group modes: clickable group labels (with `href`) and plain title labels (without `href`). */
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="Examples" href="#examples-demo">
<TableOfContents.Item className="cursor-pointer">
Basic example
</TableOfContents.Item>
<TableOfContents.Item className="cursor-pointer">
Advanced example
</TableOfContents.Item>
</TableOfContents.Group>
<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" href="#api-demo">
<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 "@photon-ai/kumo-solid";
export function TableOfContentsWithoutTitleDemo() {
return (
<DemoWrapper>
<TableOfContents>
<TableOfContents.List>
{headings.slice(0, 3).map((heading) => (
<TableOfContents.Item
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 { createSignal } from "solid-js";
import { TableOfContents } from "@photon-ai/kumo-solid";
/** Demonstrates using the `render` prop with a custom link component. */
export function TableOfContentsRenderPropDemo() {
const [clicked, setClicked] = createSignal<string | null>(null);
return (
<DemoWrapper>
<div class="space-y-3">
<TableOfContents>
<TableOfContents.List>
{["Introduction", "Installation", "Usage"].map((text) => (
<TableOfContents.Item
render={(renderProps) => (
<button {...renderProps} type="button" />
)}
onClick={() => setClicked(text)}
active={text === "Introduction"}
>
{text}
</TableOfContents.Item>
))}
</TableOfContents.List>
</TableOfContents>
{clicked() && (
<p class="text-xs text-kumo-subtle">Clicked: {clicked()}</p>
)}
</div>
</DemoWrapper>
);
}Solid Router
import { A } from "@solidjs/router";
<TableOfContents.Item
render={(props) => <A {...props} href="/intro" />}
active
>
Introduction
</TableOfContents.Item>Button (no navigation)
<TableOfContents.Item
render={(props) => <button {...props} type="button" />}
onClick={handleClick}
>
Introduction
</TableOfContents.Item>API Reference
TableOfContents
Root nav container with a default aria-label of “Table of contents”.
| Prop | Type | Default |
|---|---|---|
| children | JSX.Element | - |
| className | string | - |
| id | string | - |
| lang | string | - |
| title | string | - |
TableOfContents.Title
Optional uppercase heading displayed above the list (renders a <p>).
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
TableOfContents.List
List container with a left border rail.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
TableOfContents.Item
Individual navigation link. Set active for the current section. Use the
render prop to swap the anchor for a router link or button.
| Prop | Type | Default | Description |
|---|---|---|---|
| active | boolean | - | Whether this item represents the currently active section. |
| render | JSX.Element | - | Custom element to render as the link. Use this to integrate with framework routers (e.g., Solid Router `<A>`). The element receives all anchor props including `href`, `className`, and `children`. |
TableOfContents.Group
Groups items under a labeled section with indented children. Pass href to make the label a clickable link, or omit for a plain title.
| Prop | Type | Default | Description |
|---|---|---|---|
| label* | string | - | Label displayed above the group's items. |
| href | string | - | URL the group label links to. When provided, the label renders as a clickable link with item styling. |
| active | boolean | - | Whether this group's label represents the currently active section. Only applies when `href` is provided. |