Contents rail
An “On this page” rail built from a page's own headings, so it can never drift from them. It sticks beside the article, marks the section you are reading, and folds into a dropdown when there is no room. The rail on the right of this page is it.
Markup
Put the content and an empty <aside class="_78-toc"> side by side in a
_78-toc-layout. On load, _78.toc lists the content's h2 and
h3 headings in the aside, gives every heading (or the <section> it opens)
an id if it lacks one, and adds the same list as a dropdown at the top of the content.
<div class="_78-toc-layout">
<article id="doc">
<h2>Install</h2> …
<h2>Theming</h2> …
<h3>Tokens</h3> …
</article>
<aside class="_78-toc" data-toc-scope="#doc"></aside>
</div>
The live example below is a layout of its own, narrower than the 880px the rail needs, so it shows the other form: the dropdown. Open it and choose a section.
Before you start
Create an account and a workspace.
Import your data
A CSV with a header row.
Invite the team
Everyone gets a viewer seat by default.
<div class="_78-toc-layout">
<div id="toc-mini">
<h4>Before you start</h4>
<p class="_78-text-dim">Create an account and a workspace.</p>
<h4>Import your data</h4>
<p class="_78-text-dim">A CSV with a header row.</p>
<h4>Invite the team</h4>
<p class="_78-text-dim">Everyone gets a viewer seat by default.</p>
</div>
<aside class="_78-toc" data-toc-scope="#toc-mini" data-toc-headings="h4"></aside>
</div>
How it behaves
-
Rail or dropdown. Both are always in the DOM; a container query on
_78-toc-layoutshows the rail once the layout is 880px wide and the dropdown below that. Because it measures the layout rather than the window, collapsing this site's sidebar to its rail is enough to bring the contents rail in — try it. Without container-query support the dropdown shows. -
The current section is the last heading you have scrolled past a line just under the
top — not whichever heading is on screen — so a long section stays current all the way
down. At the very bottom of the page the last entry is current. The link gets
aria-current="location". -
Clicking scrolls smoothly (instantly with reduced motion), parks the heading just below
the topbar, writes the hash with
replaceState, and closes the dropdown. -
Fewer than two headings and the aside is
hidden: no rail, no dropdown, no empty column. -
Under a
_78-topbarthe rail sticks below it and the reading line follows it, automatically. With a fixed header of your own, set--_78-toc-topto its height.
Options
Set them as data-toc-* attributes on the aside, or pass them to _78.toc.mount().
| Attribute | Option | Default | What it does |
|---|---|---|---|
data-toc-scope | scope | the layout's other child, else <main> | The element whose headings are listed (selector or element). |
data-toc-headings | headings | h2, h3 | Selector run inside the scope. Headings inside a <dialog>, a modal or a [data-toc-skip] subtree are always skipped. |
data-toc-min | min | 2 | Fewest headings worth a rail. |
data-toc-label | label | On this page | The rail's label, the dropdown's summary, and both navs' accessible name. |
data-toc-inline | inline | start | Where the dropdown goes: after the element this selector finds, start (top of the scope) or none. |
data-toc-offset | offset | --_78-toc-top | Pixels from the top of the viewport to the reading line's base. |
This site's own rail lists only structural headings — a direct child of the article or of one of its sections — so a heading inside a demo never shows up as a section of the page:
<aside class="_78-toc" data-toc-scope=".site-doc" data-toc-inline=".site-doc__lead"
data-toc-headings=":scope > h2, :scope > h3, :scope > section > h2, :scope > section > h3"></aside>
JavaScript
// every ._78-toc mounts itself on load; for content added later:
var toc = _78.toc.mount(document.querySelector('#my-toc'), { scope: '#doc', min: 3 });
toc.items // [{ id, text, level, el }, …]
toc.current // the id of the section being read
toc.refresh() // recompute after the content changed size
toc.destroy() // remove the listeners, the list and the dropdown
document.addEventListener('_78:tocchange', function (e) {
console.log(e.detail.id); // fires whenever the current section changes
});
Classes
| Class | What it is |
|---|---|
_78-toc-layout | Flex row, content then rail, and the container the rail-or-dropdown query measures. |
_78-toc | The aside: 208px, sticky, scrolls on its own if the list is long. Outside a layout it always shows. |
_78-toc-label | The small uppercase label above the list. |
_78-toc-list / _78-toc-link | The list and its links; _78-toc-link-sub indents an h3 (and deeper). |
_78-toc-inline | The dropdown: a _78-details _78-details-sm. |
--_78-toc-top | The height of a fixed header above the page; the topbar's --header-h when there is one. |