Skip to content

TocList

TocList lists a post’s h2 and h3 headings as links, and marks the one being read. TocButton is the round Contents button that opens the list in a sheet where the rail is hidden.

On a post with at least one h2 or h3. Leave it out when there are none: an empty contents list is noise.

---
const { Content, headings } = await render(post);
const toc = headings.map((h) => ({ id: h.slug, text: h.text, level: h.depth }));
---
<SidebarCard title="On this page">
<TocList items={toc} />
</SidebarCard>
<FloatingControls>
<TocButton items={toc} />
</FloatingControls>

Levels other than 2 and 3 are dropped, so the headings from render() can go in as they are. maxHeight caps a long list in a sticky rail; the list then scrolls itself, never the page, to keep the current link in view.

Links in a column, muted, 14px. An h3 is indented, its mark with it. The current link is coral, with a 2px coral mark at its left, at the heading’s level.

TocButton is an IconButton with the list icon. It opens a MenuPanel from the right with “On this page” at the top.

Prop Values
tone (TocList) cream (default) or dark
maxHeight (TocList) A length; the list scrolls past it
devices (TocButton) The screens that show the button, ["mobile", "tablet"] by default: below 1080px, where ContentGrid hides the right rail
label, title (TocButton) The button’s name, “Contents”, and the sheet’s heading, “On this page”
State What changes
Current The last heading within 96px of the window’s top, the nav’s height and 32px more; the first before any is; the last at the end of the page
Hover The link turns ink
A link in the sheet followed The sheet closes and the page goes to the heading

The list is a <nav> named “On this page”, and the current link has aria-current="location". The links are plain fragment links, so the list works without its script. TocButton’s sheet is a modal <dialog> named “Contents”: the focus stays inside it, Escape closes it, and the button’s aria-expanded follows it.

TocList.astro and TocButton.astro. TocList is ported from the handoff, but tracks the scroll itself instead of taking activeId. The handoff has no TocButton: its FloatingControls takes an IconButton that the page wires to a sheet, and TocButton is the two in one. For it, MenuPanel gained label, which names the dialog and its close button.

FloatingControls, ContentGrid, SidebarCard.