78 UI Kit
Components

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.

_78-toc-layout
<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.

a rail too narrow to show: the dropdown
<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-layout shows 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-topbar the rail sticks below it and the reading line follows it, automatically. With a fixed header of your own, set --_78-toc-top to its height.

Options

Set them as data-toc-* attributes on the aside, or pass them to _78.toc.mount().

AttributeOptionDefaultWhat it does
data-toc-scopescopethe layout's other child, else <main>The element whose headings are listed (selector or element).
data-toc-headingsheadingsh2, h3Selector run inside the scope. Headings inside a <dialog>, a modal or a [data-toc-skip] subtree are always skipped.
data-toc-minmin2Fewest headings worth a rail.
data-toc-labellabelOn this pageThe rail's label, the dropdown's summary, and both navs' accessible name.
data-toc-inlineinlinestartWhere the dropdown goes: after the element this selector finds, start (top of the scope) or none.
data-toc-offsetoffset--_78-toc-topPixels 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:

this site's rail
<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

_78.toc
// 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

ClassWhat it is
_78-toc-layoutFlex row, content then rail, and the container the rail-or-dropdown query measures.
_78-tocThe aside: 208px, sticky, scrolls on its own if the list is long. Outside a layout it always shows.
_78-toc-labelThe small uppercase label above the list.
_78-toc-list / _78-toc-linkThe list and its links; _78-toc-link-sub indents an h3 (and deeper).
_78-toc-inlineThe dropdown: a _78-details _78-details-sm.
--_78-toc-topThe height of a fixed header above the page; the topbar's --header-h when there is one.