For AI agents: The complete documentation index is available at /llms.txt. A markdown version of this page is available at /layout/toc.md.

Table of Contents

On this page

An Astro integration that generates an “On This Page” table of contents by scanning the final rendered HTML at build time. Includes a scroll spy script that highlights the active section as the user scrolls. Optionally renders a collapsible inline TOC for narrow screens where the sidebar TOC is hidden.

Current version: 1.1.0 --npm

Installation

$ npm install astro-better-toc

Setup

Register the integration

In astro.config.ts:

import tocSmol from 'astro-better-toc';

export default defineConfig({
  integrations: [
    tocSmol({
      articleSelector: ['article.prose', 'main'],
    }),
  ],
});

Place the sidebar TOC component

In your layout, add the <TOC> component inside a [data-toc-aside] wrapper. The component hides the wrapper automatically when the page has no headings.

---
import TOC from 'astro-better-toc/TOC.astro';
---
<aside data-toc-aside>
  <p>On This Page</p>
  <TOC />
</aside>

Add the inline mobile TOC (optional)

For narrow screens where the sidebar TOC is hidden, add <InlineTOC> inside the content area, after your page heading. It renders a collapsible <details> element and disappears on viewports where the sidebar is visible. Style it with the .toc-inline family of CSS classes (see Styling below).

---
import InlineTOC from 'astro-better-toc/InlineTOC.astro';
---
<main>
  <h1>{title}</h1>
  <InlineTOC />
  <slot />
</main>

The <details> element on this page is a live example — resize the window below 1280px to see it appear.

Integration options

OptionTypeDefaultDescription
articleSelectorstring[]['article', 'main']CSS selectors tried in order to find the content area

TOC component props

PropTypeDefaultDescription
maxDepthnumber4Maximum heading depth to include (h2=2, h3=3, …)
classstring—Additional CSS classes on the TOC container

InlineTOC component props

PropTypeDefaultDescription
maxDepthnumber4Maximum heading depth to include

Styling

The InlineTOC component uses three CSS classes you define in your stylesheet:

ClassPurpose
.toc-inlineThe <details> wrapper. Hide at wide breakpoints (display: none at your TOC-visible width).
.toc-inline-summaryThe <summary> trigger row.
.toc-inline-bodyThe container wrapping the <nav>.

The component adds not-prose directly on the element to prevent Tailwind Typography link styles from bleeding in.

Minimal example:

.toc-inline { border: 1px solid #ddd; border-radius: 6px; overflow: hidden; }
@media (min-width: 1280px) { .toc-inline { display: none; } }
.toc-inline[hidden] { display: none; }
.toc-inline a { border-bottom: none; }
.toc-inline ul { list-style: none; padding: 0 !important; }
.toc-inline ul > * + * { margin-top: 0 !important; }
.toc-inline-summary { display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.75rem; cursor: pointer; }
.toc-inline-summary svg { transition: transform 0.2s; }
.toc-inline[open] .toc-inline-summary svg { transform: rotate(90deg); }
.toc-inline-body { padding: 0.25rem 0.75rem 0.5rem; }

How it works

The integration hooks into astro:build:done and walks every .html file in the output. For each file, it finds the content area matching articleSelector, extracts headings, and injects the same TOC HTML into every [data-server-toc] element on the page. This means both the sidebar <TOC> and the inline <InlineTOC> receive identical content from a single pass.

The scroll spy in <TOC> is scoped to #toc-container, so it only highlights items in the sidebar. It never interacts with the inline accordion.

Empty pages

If a page has no headings, any [data-toc-inline] elements are set to hidden in the generated HTML, and the sidebar [data-toc-aside] is hidden via JavaScript on DOMContentLoaded. Neither widget appears on pages with no navigable sections.