Relevant source files
The following files were used as context for generating this wiki page:
- packages/base-ui/src/layouts/notebook/slots/sidebar.tsx
- packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx
- packages/core/src/toc.tsx
- packages/base-ui/src/components/toc/clerk.tsx
- packages/base-ui/src/components/toc/default.tsx
- packages/radix-ui/src/components/toc/default.tsx
- packages/base-ui/src/layouts/flux/page/slots/toc.tsx
- packages/radix-ui/registry/index.ts
- packages/radix-ui/src/components/toc/clerk.tsx
- packages/base-ui/src/layouts/notebook/page/slots/toc.tsx
- packages/core/src/mdx-plugins/rehype-toc.ts
- packages/radix-ui/src/layouts/flux/page/slots/toc.tsx
- packages/base-ui/registry/index.ts
- packages/radix-ui/src/layouts/notebook/page/slots/toc.tsx
- packages/api-docs/tsdown.config.ts
- packages/base-ui/tsdown.config.ts
- packages/radix-ui/tsdown.config.ts
- packages/base-ui/src/components/toc/index.tsx
- packages/preview/src/pages/...slugs.tsx
- packages/base-ui/src/layouts/flux/slots/sidebar.tsx
- packages/radix-ui/src/components/toc/index.tsx
- packages/core/src/mdx-plugins/remark-structure.ts
- packages/core/src/mdx-plugins/remark-heading.ts
- packages/basehub/src/toc.tsx
- packages/radix-ui/src/components/inline-toc.tsx
- packages/base-ui/src/components/inline-toc.tsx
- packages/core/src/content/toc.ts
- packages/openapi/src/utils/pages/to-static-data.ts
- packages/sanity/src/client.ts
- packages/base-ui/src/contexts/tree.tsx
Overview
Fumadocs provides a table of contents (TOC) architecture that extracts heading hierarchies from Markdown ASTs, headless CMS content, and OpenAPI sources, and renders interactive, synchronized navigation lists across documentation layouts. The system handles active heading observation via IntersectionObserver, drives scroll-into-view behavior, and powers dynamic SVG thumb indicators and collapsible inline elements.
Sources: packages/core/src/toc.tsx:1-237, packages/core/src/mdx-plugins/remark-heading.ts:1-99, packages/base-ui/src/components/toc/default.tsx:30-143
TOC extraction and data structures
Data Models and Core Types
The foundational data model for table of contents entries in Fumadocs is defined by the TOCItemType interface, which represents an individual heading or navigation link. A complete table of contents collection is typed as TableOfContents, representing an array of these items.
export interface TOCItemType {
title: ReactNode;
url: string;
depth: number;
/** [remark-steps] the step number */
_step?: number;
}
export type TableOfContents = TOCItemType[];Sources: packages/core/src/toc.tsx:18-26
Additionally, runtime tracking wraps these static items into TOCItemInfo objects to manage intersection state, timestamps, and viewport fallbacks.
| Property | Type | Default | Purpose |
|---|---|---|---|
id | string | required | Unique DOM identifier extracted from the URL anchor |
active | boolean | required | Indicates whether the heading is currently active in the viewport |
t | number | required | Timestamp of the last time the item state was updated |
fallback | boolean | required | Flags items that are currently active but not actively intersecting in the viewport |
original | TOCItemType | required | Reference to the underlying static TOC item definition |
Sources: packages/core/src/toc.tsx:28-36
Note
The _step optional property on TOCItemType allows remark-steps plugins to attach sequential numbering directly to TOC nodes, enabling specialized step-based navigation renderers.
Sources: packages/core/src/toc.tsx:18-24
Markdown AST Extraction Pipeline
Table of contents extraction from Markdown and MDX documents is driven by the remark ecosystem via getTableOfContents. The extraction function accepts content and optional remark plugins, processing them synchronously or asynchronously while applying remarkHeading to harvest heading nodes from the syntax tree.
export function getTableOfContents(
content: Compatible,
remarkPlugins?: PluggableList,
): TOCItemType[] | Promise<TOCItemType[]> {
if (remarkPlugins) {
return remark()
.use(remarkPlugins)
.use(remarkHeading)
.process(content)
.then((result) => {
if ('toc' in result.data) return result.data.toc as TOCItemType[];
return [];
});
}
const result = remark().use(remarkHeading).processSync(content);
if ('toc' in result.data) return result.data.toc as TOCItemType[];
return [];
}Sources: packages/core/src/content/toc.ts:25-46
Warning
If the processed VFile result.data object does not contain a toc property populated by remarkHeading, getTableOfContents silently returns an empty array instead of throwing an error.
Sources: packages/core/src/content/toc.ts:34-45
CMS and OpenAPI Source Adapters
Fumadocs also normalizes table of contents data structures from external sources such as Sanity CMS and OpenAPI specifications. The renderToc utility transforms Portable Text blocks into standard TOCItemType entries by mapping block levels, generating anchor keys, and rendering block titles.
export function renderToc(opts: {
toc: PortableTextBlock[];
render: (body: PortableTextBlock) => ReactNode;
}): TOCItemType[] {
const { render, toc } = opts;
return toc.map(
(item): TOCItemType => ({
depth: Number(item.level ?? 0),
title: render({ ...item, style: undefined }),
url: `#${item._key}`,
}),
);
}Sources: packages/sanity/src/client.ts:5-18
For API documentation generated from OpenAPI schemas, toStaticData iterates through page operations and webhooks, building structured headings and table of contents entries using GitHub sluggers.
export function toStaticData(
page: GeneratedPageProps,
doc: Document,
): {
toc: TOCItemType[];
structuredData: StructuredData;
} {
const slugger = new Slugger();
const toc: TOCItemType[] = [];
const structuredData: StructuredData = { headings: [], contents: [] };
function pathItem(item: OperationObject, defaultTitle: string) {
if (page.showTitle && item.operationId) {
const title = item.summary || (item.operationId ? idToTitle(item.operationId) : defaultTitle);
const id = slugger.slug(title);
toc.push({
depth: 2,
title,
url: `#${id}`,
});
structuredData.headings.push({
content: title,
id,
});
}Sources: packages/openapi/src/utils/pages/to-static-data.ts:9-35
Active heading observation primitives
Active Anchor Hooks and Selectors
Core observer mechanisms, scroll listeners, and active anchor tracking hooks manage the runtime intersection of document headings. The Observer class maintains the collection of items, observer instances, and change listeners, while React hooks wrap state subscriptions and auto-scroll behaviors.
Sources: packages/core/src/toc.tsx:122-130, packages/core/src/toc.tsx:140-144, packages/core/src/toc.tsx:162-170
The observation lifecycle begins within AnchorProvider, which instantiates a single Observer via useMemo and configures its single-item mode and intersection threshold.
export function AnchorProvider({ toc, single = false, children }: AnchorProviderProps) {
const observer = useMemo(() => new Observer(), []);
observer.single = single;
useEffect(() => {
observer.setItems(toc);
}, [observer, toc]);
useEffect(() => {
observer.watch({
threshold: 0.9,
});
return () => observer.unwatch();
}, [observer]);
return <ObserverContext value={observer}>{children}</ObserverContext>;
}Sources: packages/core/src/toc.tsx:66-83
Caution
Attempting to use any table of contents hook or TOCItem outside of an active <AnchorProvider /> component will trigger an invariant error thrown by useObserver().
Sources: packages/core/src/toc.tsx:140-144
The useTOCSelector primitive subscribes components to state updates emitted by the Observer instance, running selector functions and memoizing equality checks via isEqualShallow. Specific hooks like useActiveAnchor, useActiveAnchors, and useItems derive targeted state views from the primary item collection.
| Hook Name | Return Type | Selection Logic |
|---|---|---|
useActiveAnchor() | string | undefined | Returns the ID of the most recently updated active heading (item.t > out.t) |
useActiveAnchors() | string[] | Collects an array of all IDs where item.active evaluates to true |
useItems() | TOCItemInfo[] | Returns the complete array of TOCItemInfo tracking items |
Sources: packages/core/src/toc.tsx:172-219
Tip
When implementing custom sidebar highlighting or custom rendering widgets, prefer useTOCSelector with a custom equality function to prevent unnecessary React re-renders on every scroll tick.
Sources: packages/core/src/toc.tsx:172-184
TOC component hierarchy and providers
Core and UI Provider Layer
The architecture divides table of contents management into core calculation logic in fumadocs-core/toc and framework-specific wrapper components in fumadocs-ui/base-ui and fumadocs-ui/radix-ui. Both UI packages re-export identical provider structures and scroll area primitives that bridge Primitive.AnchorProvider with React context propagation for static item arrays.
const TOCContext = createContext<Primitive.TOCItemType[]>([]);
export function useTOCItems(): Primitive.TOCItemType[] {
return use(TOCContext);
}
export type TOCProviderProps = Primitive.AnchorProviderProps;
export const { useActiveAnchor, useActiveAnchors, useItems } = Primitive;
export function TOCProvider({ toc, children, ...props }: TOCProviderProps) {
return (
<TOCContext value={toc}>
<Primitive.AnchorProvider toc={toc} {...props}>
{children}
</Primitive.AnchorProvider>
</TOCContext>
);
}Sources: packages/base-ui/src/components/toc/index.tsx:7-25, packages/radix-ui/src/components/toc/index.tsx:7-25
The TOCProvider component accepts toc items and optional properties such as single, injecting both a local TOCContext containing raw items and a Primitive.AnchorProvider context down the React component tree.
export function TOCProvider({ toc, children, ...props }: TOCProviderProps) {
return (
<TOCContext value={toc}>
<Primitive.AnchorProvider toc={toc} {...props}>
{children}
</Primitive.AnchorProvider>
</TOCContext>
);
}Sources: packages/base-ui/src/components/toc/index.tsx:17-25, packages/radix-ui/src/components/toc/index.tsx:17-25
Note
useTOCItems() consumes TOCContext to retrieve the static TOCItemType[] array directly, whereas core hooks like useItems() derive state from the active Observer instance.
Sources: packages/base-ui/src/components/toc/index.tsx:7-11, packages/radix-ui/src/components/toc/index.tsx:7-11
Scroll Areas and Context Binding
The layout incorporates TOCScrollArea to manage container-based auto-scrolling when active headings change. It establishes a viewRef reference on the scrollable container div and forwards it to Primitive.ScrollProvider.
export function TOCScrollArea({ ref, className, ...props }: ComponentProps<'div'>) {
const viewRef = useRef<HTMLDivElement>(null);
return (
<div
ref={mergeRefs(viewRef, ref)}
className={cn(
'relative min-h-0 text-sm ms-px overflow-auto [scrollbar-width:none] mask-[linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)] py-3',
className,
)}
{...props}
>
<Primitive.ScrollProvider containerRef={viewRef}>{props.children}</Primitive.ScrollProvider>
</div>
);Sources: packages/base-ui/src/components/toc/index.tsx:27-41, packages/radix-ui/src/components/toc/index.tsx:27-41
The table below lists the components, providers, and shared hooks exported across the core and UI packages:
| Identifier | Source Package | Role & Description |
|---|---|---|
AnchorProvider | fumadocs-core/toc | Initializes the Observer class, manages single-item selection modes, and supplies ObserverContext |
ScrollProvider | fumadocs-core/toc | Supplies ScrollContext with the container reference for auto-scrolling active items into view |
TOCProvider | fumadocs-ui/* | Combines TOCContext array storage with Primitive.AnchorProvider wrapping |
TOCScrollArea | fumadocs-ui/* | Renders the styled container (overflow-auto, [scrollbar-width:none], mask gradients) hosting ScrollProvider |
useTOCItems() | fumadocs-ui/* | Accesses the static array of TOCItemType objects from TOCContext |
Sources: packages/core/src/toc.tsx:61-83, packages/base-ui/src/components/toc/index.tsx:7-41, packages/radix-ui/src/components/toc/index.tsx:7-41
Warning
TOCScrollArea applies [scrollbar-width:none] and vertical CSS mask gradients (mask-[linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)]); overriding overflow properties without preserving the container reference will break scrollIntoView auto-centering.
Sources: packages/base-ui/src/components/toc/index.tsx:31-40, packages/radix-ui/src/components/toc/index.tsx:31-40
Indicator thumb tracking and rendering
SVG Layout Calculations and Path Generation
Sources: packages/base-ui/src/components/toc/clerk.tsx:28-126, packages/base-ui/src/components/toc/clerk.tsx:137-185, packages/core/src/toc.tsx:147-160, packages/core/src/toc.tsx:140-144
The table of contents indicator renders dynamic SVG track lines alongside active item thumbs using layout measurements derived from container DOM queries. Within TOCItems, an onPrint callback measures each item element via container.querySelector('a[href="' + item.url + '"]') and computes layout coordinates.
for (let i = 0; i < items.length; i++) {
const item = items[i];
const element: HTMLElement | null = container.querySelector(`a[href="${item.url}"]`);
if (!element) continue;
const styles = getComputedStyle(element);
const x = getLineOffset(item.depth) + 0.5;
const top = element.offsetTop + parseFloat(styles.paddingTop);
const bottom = element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom);
w = Math.max(x + 8, w);
h = Math.max(h, bottom);
if (i === 0) {
d += ` M${x} ${top} L${x} ${bottom}`;
} else {
const [, upperBottom, upperX] = i > 0 ? positions[i - 1] : [0, 0, 0];
d += ` C ${upperX} ${top - 4} ${x} ${upperBottom + 4} ${x} ${top} L${x} ${bottom}`;
}
if (item._step !== undefined) {
output.push(
<g key={i} transform={`translate(${x}, ${(top + bottom) / 2})`}>
<circle cx="0" cy="0" r="8" className="fill-fd-primary" />
<text
cx="0"
cy="0"
textAnchor="middle"
alignmentBaseline="central"
dominantBaseline="middle"
className="fill-fd-primary-foreground font-medium text-xs leading-none font-mono rtl:-scale-x-100"
>
{item._step}
</text>
</g>,
);
}
positions.push([top, bottom, x]);
}Sources: packages/base-ui/src/components/toc/default.tsx:48-88, packages/radix-ui/src/components/toc/default.tsx:48-88
Note
The default variant path uses cubic bezier commands to smoothly bridge line segments across varying heading depths, whereas the Clerk variant employs straight line segments.
Sources: packages/base-ui/src/components/toc/default.tsx:63-67, packages/base-ui/src/components/toc/clerk.tsx:61-65, packages/radix-ui/src/components/toc/default.tsx:63-67, packages/radix-ui/src/components/toc/clerk.tsx:61-65
Thumb Track Clip Paths and Variants
The ThumbTrack subcomponent reads active states via Primitive.useTOC() and Primitive.useTOCListener(), dynamically updating CSS custom properties --track-top and --track-bottom on the container element.
function ThumbTrack({ computed }: { computed: ComputedSVG }) {
const ref = useRef<HTMLDivElement>(null);
const tocInfo = Primitive.useTOC();
function calculate(items: Primitive.TOCItemInfo[]) {
const out: Record<string, string> = {};
const startIdx = items.findIndex((item) => item.active);
if (startIdx === -1) return out;
const endIdx = items.findLastIndex((item) => item.active);
out['--track-top'] = `${computed.positions[startIdx][0]}px`;
out['--track-bottom'] = `${computed.positions[endIdx][1]}px`;
return out;
}
Primitive.useTOCListener((items) => {
const element = ref.current;
if (!element) return;
for (const [k, v] of Object.entries(calculate(items))) {
element.style.setProperty(k, v);
}
});
return (
<div
ref={ref}
className="absolute top-0 inset-s-0 origin-center rtl:-scale-x-100"
style={{
width: computed.width,
height: computed.height,
...calculate(tocInfo.get()),
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox={`0 0 ${computed.width} ${computed.height}`}
className="absolute transition-[clip-path]"
style={{
width: computed.width,
height: computed.height,
clipPath: `polygon(0 var(--track-top,0), 100% var(--track-top,0), 100% var(--track-bottom,0), 0 var(--track-bottom,0))`,
}}
>
{computed.content}
</svg>
</div>
);
}Sources: packages/base-ui/src/components/toc/clerk.tsx:137-185, packages/radix-ui/src/components/toc/clerk.tsx:137-185
| Variant Package / File | Computed SVG Feature | Path Command / Styling Method | Step Numbering Support |
|---|---|---|---|
packages/base-ui/src/components/toc/default.tsx | Cubic path smoothing with optional thumbBox lengths | Cubic bezier path calculation | Yes (item._step !== undefined) |
packages/base-ui/src/components/toc/clerk.tsx | Straight line segment transitions with polygon clipping | Straight line segment path calculation | Yes (item._step !== undefined) |
packages/radix-ui/src/components/toc/default.tsx | Cubic path smoothing with optional thumbBox lengths | Cubic bezier path calculation | Yes (item._step !== undefined) |
packages/radix-ui/src/components/toc/clerk.tsx | Straight line segment transitions with polygon clipping | Straight line segment path calculation | Yes (item._step !== undefined) |
Sources: packages/base-ui/src/components/toc/default.tsx:63-85, packages/base-ui/src/components/toc/clerk.tsx:61-83, packages/radix-ui/src/components/toc/default.tsx:63-85, packages/radix-ui/src/components/toc/clerk.tsx:61-83
Caution
When thumbBox is enabled in the default variant, the component calculates path lengths using path.getTotalLength() and path.getPointAtLength(l). If container heights resize to zero or headings have no corresponding DOM elements, onPrint returns early and sets the SVG state to null, disabling the indicator thumb entirely.
Sources: packages/base-ui/src/components/toc/default.tsx:35-41, packages/base-ui/src/components/toc/default.tsx:96-109, packages/radix-ui/src/components/toc/default.tsx:35-41, packages/radix-ui/src/components/toc/default.tsx:96-109
Inline collapsible TOC components
Component Implementation and Styling
The inline table of contents presentation is implemented through framework-specific UI components across packages, rendered as client components utilizing @fuma-translate/react translations and Lucide icons.
'use client';
import { ChevronDown } from 'lucide-react';
import type { TOCItemType } from 'fumadocs-core/toc';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
import type { ComponentProps } from 'react';
import { cn } from '@/utils/cn';
import { useTranslations } from '@fuma-translate/react';
export interface InlineTocProps extends ComponentProps<typeof Collapsible> {
items: TOCItemType[];
}Sources: packages/radix-ui/src/components/inline-toc.tsx:1-12, packages/base-ui/src/components/inline-toc.tsx:1-12
The InlineTOC component accepts an array of TOCItemType objects alongside standard component properties, wrapping content inside collapsible primitives with a customizable header trigger.
export function InlineTOC({ items, ...props }: InlineTocProps) {
const t = useTranslations({ note: 'inline table of contents' });
return (
<Collapsible
{...props}
className={cn(
'not-prose rounded-lg border bg-fd-card text-fd-card-foreground',
props.className,
)}
>
<CollapsibleTrigger className="group inline-flex w-full items-center justify-between px-4 py-2.5 font-medium">
{props.children ?? t('Table of Contents')}
<ChevronDown className="size-4 transition-transform duration-200 group-data-[state=open]:rotate-180" />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col p-4 pt-0 text-sm text-fd-muted-foreground">
{items.map((item) => (
<a
key={item.url}
href={item.url}
className="border-s py-1.5 hover:text-fd-accent-foreground"
style={{
paddingInlineStart: 12 * Math.max(item.depth - 1, 0),
}}
>
{item.title}
</a>
))}
</div>
</CollapsibleContent>
</Collapsible>
);Sources: packages/radix-ui/src/components/inline-toc.tsx:14-46, packages/base-ui/src/components/inline-toc.tsx:14-48
| Package Path | Class Name Handling | Trigger Chevron Animation | Translation Key |
|---|---|---|---|
packages/radix-ui/src/components/inline-toc.tsx | cn('not-prose rounded-lg border bg-fd-card text-fd-card-foreground', props.className) | group-data-[state=open]:rotate-180 | Table of Contents |
packages/base-ui/src/components/inline-toc.tsx | cn(..., typeof className === 'function' ? className(s) : className) | group-data-open:rotate-180 | Table of Contents |
Sources: packages/radix-ui/src/components/inline-toc.tsx:20-27, packages/base-ui/src/components/inline-toc.tsx:20-29
Note
Indentation for nested TOC items inside InlineTOC is calculated dynamically via inline styles using 12 * Math.max(item.depth - 1, 0) for paddingInlineStart, coupled with a vertical border border-s on each anchor element.
Sources: packages/radix-ui/src/components/inline-toc.tsx:31-41, packages/base-ui/src/components/inline-toc.tsx:33-43
Layout slot integration and popovers
Flux and Notebook TOC Slot Implementation
Documentation layouts integrate table of contents elements into specialized layout slots and collapsible popovers. In the Flux layout architecture, TOC mounts via a React portal targeting the DOM element with the ID #flux-layout-slot, wrapping its contents within PageTOCPopover and PageTOCPopoverPhysical to manage click-outside events and expansion states.
function PageTOCPopover(props: ComponentProps<'div'>) {
const [container, setContainer] = useState<HTMLElement | null>(null);
useEffect(() => {
const element = document.getElementById('flux-layout-slot');
if (!element) return;
setContainer(element);
}, []);
if (!container) return;
return createPortal(<PageTOCPopoverPhysical {...props} />, container);
}Sources: packages/base-ui/src/layouts/flux/page/slots/toc.tsx:92-103, packages/radix-ui/src/layouts/flux/page/slots/toc.tsx:92-103
The notebook layout splits table of contents handling into a desktop sticky sidebar container (#nd-toc) and a collapsible mobile popover (TOCPopover). When no table of contents items, headers, or footers are present, the desktop layout returns a hidden placeholder div with fixed grid width configuration.
export function TOC({ container, header, footer, style = 'normal', list }: TOCProps) {
const t = useTranslations({ note: 'table of contents' });
const items = Base.useTOCItems();
const { TOCItems, TOCEmpty, TOCItem } = style === 'clerk' ? TocClerk : TocDefault;
if (items.length === 0 && !footer && !header) {
return <div id="nd-toc-placeholder" className="hidden xl:layout:[--fd-toc-width:268px]" />;
}Sources: packages/base-ui/src/layouts/notebook/page/slots/toc.tsx:51-58, packages/radix-ui/src/layouts/notebook/page/slots/toc.tsx:51-58
Popover Triggers and Progress Indicators
Layout popover triggers incorporate progress circles and animated text transitions using motion.span components. The progress calculation evaluates the active item index relative to total list items, ensuring smooth visual tracking of reading position.
<ProgressCircle
value={(items.findLastIndex((item) => item.active) + 1) / Math.max(1, items.length)}
max={1}
className={cn('shrink-0', open && 'text-fd-primary')}
/>Sources: packages/base-ui/src/layouts/flux/page/slots/toc.tsx:188-192, packages/radix-ui/src/layouts/flux/page/slots/toc.tsx:188-192
Warning
In PageTOCPopoverPhysical, window click listeners check whether target elements fall outside ref.current. If a portal-rendered trigger or external custom menu sits outside the popover header element, clicking it triggers an immediate collapse via setOpen(false).
Sources: packages/base-ui/src/layouts/flux/page/slots/toc.tsx:109-121, packages/radix-ui/src/layouts/flux/page/slots/toc.tsx:109-121
Related
On This Page
OverviewTOC extraction and data structuresData Models and Core TypesMarkdown AST Extraction PipelineCMS and OpenAPI Source AdaptersActive heading observation primitivesActive Anchor Hooks and SelectorsTOC component hierarchy and providersCore and UI Provider LayerScroll Areas and Context BindingIndicator thumb tracking and renderingSVG Layout Calculations and Path GenerationThumb Track Clip Paths and VariantsInline collapsible TOC componentsComponent Implementation and StylingLayout slot integration and popoversFlux and Notebook TOC Slot ImplementationPopover Triggers and Progress IndicatorsRelated