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 next to the article |
'auto' | Card at the top of the article | Sticky sidebar next to 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 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.
Left or right?
Whenever the sidebar is active ('sidebar' or 'auto'), you can pick which side it sits on with the sidebarPosition setting. 'left' puts the TOC before the article — handy when you want the navigation to feel like a docs site or to mirror your main nav. 'right' keeps it after the article, which is the more common blog convention. The article column stays the same width either way; only the column order changes.
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'
sidebarPosition: 'left', // 'left' | 'right' (sidebar layouts only)
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 — and on the side — you chose. sidebarPosition defaults to 'right' if you omit it, and is ignored when layout is 'inline'.
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',
sidebarPosition: 'right',
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'
sidebarPosition: 'left', // or 'right'
Save and rebuild — every existing blog post picks up the new layout automatically. No frontmatter migration, no per-post tweaks. Flipping the sidebar from one side to the other is the same one-line change to sidebarPosition.
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.
Left vs. right is a pure layout swap: the sidebar <aside> is rendered before or after the article in the DOM, and the grid template flips between [15rem_minmax(0,1fr)] and [minmax(0,1fr)_15rem]. No extra CSS, no extra JS — and the same scroll-spy script runs in either direction.
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, a left-or-right sidebar, one config block, an optional per-post override — and a scroll-spy that just works.