Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Table of Contents

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

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.

PropertyTypeDefaultPurpose
idstringrequiredUnique DOM identifier extracted from the URL anchor
activebooleanrequiredIndicates whether the heading is currently active in the viewport
tnumberrequiredTimestamp of the last time the item state was updated
fallbackbooleanrequiredFlags items that are currently active but not actively intersecting in the viewport
originalTOCItemTyperequiredReference 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 NameReturn TypeSelection Logic
useActiveAnchor()string | undefinedReturns 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:

IdentifierSource PackageRole & Description
AnchorProviderfumadocs-core/tocInitializes the Observer class, manages single-item selection modes, and supplies ObserverContext
ScrollProviderfumadocs-core/tocSupplies ScrollContext with the container reference for auto-scrolling active items into view
TOCProviderfumadocs-ui/*Combines TOCContext array storage with Primitive.AnchorProvider wrapping
TOCScrollAreafumadocs-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 / FileComputed SVG FeaturePath Command / Styling MethodStep Numbering Support
packages/base-ui/src/components/toc/default.tsxCubic path smoothing with optional thumbBox lengthsCubic bezier path calculationYes (item._step !== undefined)
packages/base-ui/src/components/toc/clerk.tsxStraight line segment transitions with polygon clippingStraight line segment path calculationYes (item._step !== undefined)
packages/radix-ui/src/components/toc/default.tsxCubic path smoothing with optional thumbBox lengthsCubic bezier path calculationYes (item._step !== undefined)
packages/radix-ui/src/components/toc/clerk.tsxStraight line segment transitions with polygon clippingStraight line segment path calculationYes (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 PathClass Name HandlingTrigger Chevron AnimationTranslation Key
packages/radix-ui/src/components/inline-toc.tsxcn('not-prose rounded-lg border bg-fd-card text-fd-card-foreground', props.className)group-data-[state=open]:rotate-180Table of Contents
packages/base-ui/src/components/inline-toc.tsxcn(..., typeof className === 'function' ? className(s) : className)group-data-open:rotate-180Table 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

Was this page helpful?