TableOfContents

TableOfContents builds a navigable outline from page headings.

Basics

Drop a table of contents beside your main article and it will register headings automatically through the shared title registry.

Loading demo…

import { Block, Row, TableOfContents, Text, Title, TitleRegistryProvider } from '@plocks/ui'; const SECTIONS = [ { id: 'intro', title: 'Introduction', summary: 'Set the stage for the walkthrough.' }, { id: 'setup', title: 'Setup', summary: 'Install dependencies and initialize the provider.' }, { id: 'usage', title: 'Usage', summary: 'Render headings inside your content area to register them.' }, { id: 'faq', title: 'FAQ', summary: 'Answer the questions you expect most often.' }, ]; export function Demo() { return ( <TitleRegistryProvider> <Row gap="xl" align="flex-start"> <TableOfContents container="#toc-basic-content" variant="outline" size="sm" p="sm" style={{ width: 240 }} /> <Block id="toc-basic-content" grow={1} style={{ maxWidth: 560 }}> {SECTIONS.map((section, index) => ( <Block key={section.id}> <Title order={index === 0 ? 1 : 2}>{section.title}</Title> <Text c="secondary">{section.summary}</Text> </Block> ))} </Block> </Row> </TitleRegistryProvider> ); }

Variants

Choose between the outline, ghost, filled, and none variants. Pair filled with autoContrast to keep labels legible against a brand color.

Loading demo…

import { Row, TableOfContents } from '@plocks/ui'; const ITEMS = [ { id: 'overview', value: 'Overview', depth: 1 }, { id: 'tokens', value: 'Color tokens', depth: 2 }, { id: 'accessibility', value: 'Accessibility', depth: 1 }, ]; export function Demo() { return ( <Row gap="md" align="flex-start" wrap="wrap"> <TableOfContents initialData={ITEMS} variant="outline" size="xs" style={{ width: 200 }} /> <TableOfContents initialData={ITEMS} variant="ghost" size="xs" style={{ width: 200 }} /> <TableOfContents initialData={ITEMS} variant="filled" color="primary.6" autoContrast size="xs" style={{ width: 200 }} /> </Row> ); }

Preloaded data

Seed the table of contents with initialData so servers and prerender jobs can render the navigation before headings mount.

Loading demo…

import { Block, TableOfContents } from '@plocks/ui'; const INITIAL_ITEMS = [ { id: 'overview', value: 'Overview', depth: 1 }, { id: 'setup', value: 'Setup', depth: 2 }, { id: 'usage', value: 'Usage', depth: 2 }, { id: 'advanced', value: 'Advanced', depth: 1 }, { id: 'faq', value: 'FAQ', depth: 1 }, ]; export function Demo() { return ( <Block align="flex-start"> <TableOfContents initialData={INITIAL_ITEMS} variant="outline" depthOffset={16} radius="sm" size="sm" p="sm" style={{ width: 240 }} /> </Block> ); }

Depth offset

Use minDepthToOffset and depthOffset to indent nested headings so deep sections are easy to scan.

Loading demo…

import { Block, TableOfContents } from '@plocks/ui'; const INITIAL_ITEMS = [ { id: 'intro', value: 'Introduction', depth: 1 }, { id: 'schedule', value: 'Release schedule', depth: 2 }, { id: 'api', value: 'API reference', depth: 2 }, { id: 'hooks', value: 'Hooks', depth: 3 }, { id: 'migration', value: 'Migration', depth: 1 }, ]; export function Demo() { return ( <Block align="flex-start"> <TableOfContents initialData={INITIAL_ITEMS} variant="outline" minDepthToOffset={2} depthOffset={28} size="xs" p="sm" style={{ width: 240 }} /> </Block> ); }

Active callbacks

Subscribe to onActiveChange to surface the currently highlighted section, perfect for syncing status chips or analytics.

Loading demo…

import { useState } from 'react'; import { Block, Chip, Row, TableOfContents, Text, Title, TitleRegistryProvider } from '@plocks/ui'; const SECTIONS = [ { id: 'overview', title: 'Overview', summary: 'Explain when the progress indicator should appear.' }, { id: 'loading', title: 'Loading States', summary: 'Describe feedback while content is fetching.' }, { id: 'error', title: 'Error Recovery', summary: 'Clarify what happens if the data fails to load.' }, ]; export function Demo() { const [activeId, setActiveId] = useState<string | null>(null); return ( <TitleRegistryProvider> <Block> <Chip variant="light" color={activeId ? 'primary' : 'gray'} size="sm"> Active section: {activeId ?? 'None'} </Chip> <Row gap="xl" align="flex-start"> <TableOfContents container="#toc-active-callback-content" variant="outline" size="xs" p="sm" style={{ width: 240 }} onActiveChange={setActiveId} /> <Block id="toc-active-callback-content" grow={1} style={{ maxWidth: 560 }}> {SECTIONS.map((section, index) => ( <Block key={section.id}> <Title order={index === 0 ? 1 : 2}>{section.title}</Title> <Text c="secondary">{section.summary}</Text> </Block> ))} </Block> </Row> </Block> </TitleRegistryProvider> ); }
Scan to open this page on your phoneplocks.dev/ui/TableOfContents