# Table of Contents

Server-side table of contents with scroll spy and optional inline mobile accordion, injected at build time.

> For the index of this section of the site, see [llms.txt](https://better-static-sites.github.io/llms.txt)

# Table of Contents

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](https://www.npmjs.com/package/astro-better-toc)

## Installation

```shell-session
$ npm install astro-better-toc
```

## Setup

### Register the integration

In `astro.config.ts`:

```typescript
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.

```jsx
---
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](#styling) below).

```jsx
---
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

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `articleSelector` | `string[]` | `['article', 'main']` | CSS selectors tried in order to find the content area |

## TOC component props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `maxDepth` | `number` | `4` | Maximum heading depth to include (h2=2, h3=3, …) |
| `class` | `string` | — | Additional CSS classes on the TOC container |

## InlineTOC component props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `maxDepth` | `number` | `4` | Maximum heading depth to include |

## Styling

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

| Class | Purpose |
| --- | --- |
| `.toc-inline` | The `<details>` wrapper. Hide at wide breakpoints (`display: none` at your TOC-visible width). |
| `.toc-inline-summary` | The `<summary>` trigger row. |
| `.toc-inline-body` | The container wrapping the `<nav>`. |

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

Minimal example:

```css
.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.