Contact Us

Table of Contents

DOC-00014 reference implementor, developer

Table of Contents

The TOC system uses three CORE-OWNED components that share a single link-list primitive and provide two purpose-specific wrappers. (D-D3-08)

ComponentFilePurpose
TocListsrc/core/components/ui/TocList.astroShared link-list primitive — no container, no JS
TocAsidesrc/core/components/ui/TocAside.astroSticky sidebar card with scroll and active links
TocInlinesrc/core/components/ui/TocInline.astroCollapsible in-page display with vanilla JS toggle

Route templates consume TocAside or TocInline directly — never TocList on its own.

Who this is for

  • Implementors wiring TOC into route templates
  • Developers customizing TOC behavior via frontmatter
  • AI agents composing detail routes with TOC support

Architecture

TocList (shared primitive)
├── TocAside (sidebar wrapper)
│   ├── Sticky positioning
│   ├── Internal scroll container
│   └── IntersectionObserver active-link tracking
└── TocInline (inline wrapper)
    ├── Collapsible vanilla JS toggle
    ├── Soft-background container
    └── No scroll, no sticky, no active links

Both wrappers delegate heading filtering and link rendering to TocList. They add their own container styling, positioning, and interactivity.

Current consumers

RouteComponentNotes
src/pages/articles/[...slug].astroTocAsideSidebar TOC with active-link tracking
src/pages/theme-docs/[...slug].astroTocInlineIn-page collapsible TOC in the main content header
src/pages/theme-docs/showcase/*.astroTocAsideSidebar TOC for showcase pages

TocList (shared primitive)

Filters headings by depth range and renders a <nav><ol> of anchor links. No container styling, no scroll, no active-link tracking.

Props

PropTypeDefaultNotes
headingsTocHeading[][]Array of heading objects from render()
labelstring"On this page"Accessible label for the <nav> element
allowEmptybooleanfalseRender the shell even with no items
startLevelnumber2Minimum heading depth to include (1–6)
endLevelnumber3Maximum heading depth to include (1–6)
classstringAdditional CSS classes

TocHeading interface

Exported from TocList.astro and imported by both wrappers:

interface TocHeading {
  depth: number; // 1–6 (h1–h6)
  slug: string; // Anchor ID for the heading
  text: string; // Plain text content
}

CSS class names

ClassElementPurpose
toc-list<nav>Root element
toc-list__items<ol>Ordered list container
toc-list__item<li>List item
toc-list__item--nested<li>Indented item (depth > minLevel)
toc-list__link<a>Anchor link
toc-list__link--active<a>Active link (set by TocAside JS)

TocAside (sidebar)

Wraps TocList in a sticky card with border, scroll container, and IntersectionObserver active-link highlighting. Used in sidebar positions alongside long-form content.

Props

All TocList props plus:

PropTypeDefaultNotes
headingLevel1–62Semantic heading level for the label
activeLinksbooleantrueEnables IntersectionObserver active-link highlighting

Sticky positioning

Uses position: sticky with top: var(--pt-layout-scroll-margin) so it follows the reader down long pages while staying below the sticky header. The card constrains its height to the viewport minus scroll margin and uses the .toc-aside__scroll region as the internal scroll container.

When activeLinks is true (default), an IntersectionObserver watches the headings targeted by each TOC link. The topmost visible heading’s link receives the toc-list__link--active class, applying link color and semibold weight.

The observer uses a setTimeout(0) defer on DOMContentLoaded to ensure it runs after any client-side fallback TOC script. Set activeLinks={false} to disable highlighting on a specific instance.

CSS class names

ClassElementPurpose
toc-aside<div>Root card element
toc-aside__labelHeadingHeading label
toc-aside__scroll<div>Internal scroll container

TocInline (collapsible)

Wraps TocList in a soft-background container with a vanilla JS collapsible toggle. Used in the main content header area of docs routes. No scroll, no sticky, no active-link tracking.

Props

All TocList props plus:

PropTypeDefaultNotes
headingLevel1–62Semantic heading level for the label

Collapse behavior

  • Starts expanded (initial state: open = true)
  • Toggle button wraps the heading label with a chevron-right icon that rotates 90° when open
  • Uses CSS grid-template-rows for smooth animation; hidden by default to prevent flash of collapsed state
  • Button uses aria-expanded and aria-controls for accessibility

How headings reach the TOC

Astro’s render() function extracts headings from markdown content at build time and returns them as a MarkdownHeading[] array. Route templates pass this array to the TOC component; depth filtering happens inside TocList.

---
const { Content, headings } = await render(entry);
---

<TocInline
  headings={headings}
  startLevel={tocStartLevel}
  endLevel={tocEndLevel}
/>

Important: render() only extracts headings authored in markdown (## Heading). Headings rendered by the Heading.astro component in .astro templates are not included — they exist in the template, not in the content.

Frontmatter controls

Content authors control TOC behavior through three frontmatter fields defined in the content schema:

FieldTypeDefault (articles/pages)Default (docs)Notes
tocbooleantruetrueSet to false to suppress TOC
tocStartLevelnumber12Lowest heading level included in TOC
tocEndLevelnumber23Highest heading level included in TOC

Example — show only H2 headings in the TOC:

---
toc: true
tocStartLevel: 2
tocEndLevel: 2
---

TOC exclusion

Individual headings can be excluded from the TOC using the data-toc="exclude" HTML attribute. The Heading primitive supports this via its toc prop:

<!-- This heading appears in the TOC (default) -->
<Heading level={2} id="visible">Visible heading</Heading>

<!-- This heading is excluded from the TOC -->
<Heading level={2} id="hidden" toc={false}>Hidden from TOC</Heading>

When toc={false}, the Heading component renders data-toc="exclude" on the heading element. The docs route client-side fallback script filters out any heading with this attribute.

Known limitation

Per-heading TOC exclusion works only for component-rendered headings (<Heading toc={false}>) in .astro templates. Markdown-authored headings (## Foo) cannot carry the data-toc="exclude" attribute because Astro’s render() returns headings as { depth, slug, text } with no custom attribute passthrough. Markdown authors can control TOC scope coarsely via tocStartLevel/tocEndLevel frontmatter, but not per-heading.

Client-side fallback (docs route)

The docs route includes a fallback mechanism for cases where render() returns no headings but the TOC is enabled. When allowEmpty is true and data-toc-fallback="true" is set on the component, a client-side script:

  1. Queries rendered headings from the .doc-body__content element (the LayoutSection <article> on docs detail routes)
  2. Filters by the configured tocStartLevel/tocEndLevel range
  3. Excludes headings with data-toc="exclude"
  4. Auto-generates id attributes for headings that lack them
  5. Populates the TOC list dynamically

This ensures the TOC works even when heading data isn’t available at the server-rendering stage.

Styling

Both wrappers use semantic and primitive tokens:

  • Surface: --st-color-surface-soft background
  • Border (TocAside only): --st-color-border-default with --pt-border-width-thin
  • Links: --st-color-text-muted default, --st-color-link-hover on hover, --st-color-surface-strong hover background
  • Focus: --st-color-focus-ring with --pt-focus-ring-width and --pt-focus-ring-offset
  • Active link (TocAside only): --st-color-link color, --pt-font-weight-semibold weight

Accessibility

  • TocList renders a <nav> with aria-label for screen reader identification
  • TOC links are keyboard-navigable with visible focus rings
  • TocInline toggle button uses aria-expanded and aria-controls
  • Heading labels use the Heading primitive with level-based size cascade for document outline correctness

Search

Search across pages and articles. Use arrow keys to navigate results.

Search across pages and articles.

Loading search...

Search is unavailable. Please try again later.

    No results for ""

    Try different keywords or fewer words.