Long-form articles benefit from a table of contents. It tells readers what’s coming, lets them jump straight to the section they want, and acts as a quiet progress indicator when scroll-spy highlights the active section.
Astro Rocket ships a built-in TOC component for blog posts with three layout options — pick the one that fits your audience. Every option is opt-in: when the TOC is disabled, no extra HTML and no extra JavaScript reach the page.
The three layouts
| Layout | Mobile / tablet (< xl) | Desktop (xl+, ≥1280 px) |
|---|---|---|
'inline' | Card at the top of the article | Card at the top of the article |
'sidebar' | TOC hidden | Sticky sidebar to the right of the article |
'auto' | Card at the top of the article | Sticky sidebar to the right of the article |
Inline keeps your full reading width on every viewport — best when you want articles to read like a book and don’t need the TOC to follow the reader. Mine started this way.
Sidebar is the classic docs-style layout — a sticky list on the right that follows the reader on desktop and stays out of the way on phones. Hidden below the xl breakpoint so it doesn’t compete with mobile reading.
Auto combines both: phone and tablet readers get the inline card at the top, desktop readers get the sticky sidebar. This is what this site uses — try resizing the browser on any blog post.
In all three, the article column stays at max-w-4xl so reading width never changes when the sidebar appears or disappears.
How to enable
Open src/config/site.config.ts and find the articleFeatures.toc block:
articleFeatures: {
toc: {
enabled: true, // turn the TOC on site-wide
layout: 'auto', // 'inline' | 'sidebar' | 'auto'
minHeadings: 3, // hide TOC on posts with fewer than 3 headings
maxDepth: 3, // include H2 + H3 (set to 2 for H2 only)
},
},
Save, build, and every blog post with three or more headings now shows a TOC in the layout you chose.
How to disable
If you’ve decided you’d rather not have a TOC at all, set enabled to false:
articleFeatures: {
toc: {
enabled: false, // ← TOC fully off
layout: 'inline',
minHeadings: 3,
maxDepth: 3,
},
},
When disabled, the TableOfContents component renders nothing, no scroll-spy script reaches the page, and the article column stays at its original max-w-4xl layout. Performance is identical to a theme without the feature.
Hide on a single post
Sometimes a post is too short for a TOC, or it’s an announcement that doesn’t need section anchors. Override on a per-post basis with frontmatter:
---
title: My short post
toc: false
---
This hides the TOC on just that post — your site-wide setting stays untouched.
You can also force-hide in the other direction: if you’ve enabled the TOC site-wide but a specific evergreen post has its own custom navigation, toc: false lets you opt that one post out without changing your config.
Switching layouts later
Switching is a one-line change. If you start with 'inline' and decide later you want a sidebar:
layout: 'sidebar', // or 'auto'
Save and rebuild — every existing blog post picks up the new layout automatically. No frontmatter migration, no per-post tweaks.
How it works under the hood
The TOC is generated from the headings Astro extracts from your MDX file. There’s no extra plugin or library — it uses the headings array that Astro already returns from render(post).
Each heading already has an id (Astro’s MDX integration auto-generates them via slugified text), so the TOC just turns those into anchor links. No external dependencies.
For auto, both an inline card and a sidebar nav are rendered on every post, with CSS xl:hidden and hidden xl:block swapping which one is visible. Scroll-spy runs independently on whichever is currently in view, using IntersectionObserver with a tuned rootMargin so a heading becomes “active” when it scrolls into the upper portion of the viewport, not when it’s already past the bottom.
What it costs (close to nothing)
| Scenario | Cost |
|---|---|
TOC disabled (enabled: false) | 0 KB, 0 JS — exactly as if the feature didn’t exist |
TOC enabled, post has fewer than minHeadings | 0 KB — component renders nothing |
TOC enabled ('inline'), post qualifies | ~1 KB HTML + ~1 KB inline JS for scroll-spy |
TOC enabled ('sidebar' or 'auto'), post qualifies | Same ~2 KB total — auto reuses the same script for both layouts |
There’s no third-party script, no external request, and no cumulative layout shift. The component is server-rendered HTML with one small inline script.
Customising the look
The TOC uses the standard theme tokens — --color-foreground, --color-foreground-muted, --color-brand-500, --color-border. Switch your colour theme and the TOC adapts. No hard-coded colours.
If you want a different title above the list, the component accepts a title prop. The default is “On this page” but you can override it inside BlogLayout.astro if your audience speaks something other than English.
Where it lives
- Component:
src/components/blog/TableOfContents.astro - Wiring:
src/layouts/BlogLayout.astro - Config:
src/config/site.config.ts→articleFeatures.toc - Per-post override:
toc: falsein MDX frontmatter
That’s the whole feature. Three layouts, one config flag, an optional per-post override — and a scroll-spy that just works.