Table of contents

nav.toc[data-ui="toc"]

Table of contents — the "on this page" list beside an article.

Examples

On this page

View “On this page” as
<nav class="toc toc-sticky" aria-labelledby="toc-heading" data-ui="toc">
  <h2 class="toc-title" id="toc-heading">On this page</h2>
  <ol class="toc-list">
    <li><a class="toc-link" href="#install">Installation</a></li>
    <li>
      <a class="toc-link" href="#tokens">Tokens</a>
      <ol class="toc-list">
        <li><a class="toc-link" href="#tokens-colour">Colour</a></li>
        <li><a class="toc-link" href="#tokens-space">Spacing</a></li>
      </ol>
    </li>
    <li><a class="toc-link" href="#skins">Skins</a></li>
  </ol>
</nav>

data-ui="toc" sets aria-current="location" on the link whose heading is on screen — an attribute, so the state is announced and not only painted.

Without the script it is a plain list of in-page links, and it claims nothing more than that.

Nesting is nesting

View “Nesting is nesting” as
<nav class="toc" aria-label="On this page">
  <ol class="toc-list">
    <li><a class="toc-link" href="#one">Overview</a></li>
    <li>
      <a class="toc-link" href="#two">Reference</a>
      <ol class="toc-list">
        <li><a class="toc-link" href="#two-a">Tokens</a></li>
      </ol>
    </li>
  </ol>
</nav>

An <ol> inside the <li> of its parent heading, one level per heading level. There is no .toc-link-2 and no depth modifier, so a document that grows an h4 needs no new class.

Tokens 20

Level 2, declared on .toc itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by toc
TokenDefault
--toc-current-colorvar(--color-primary)
--toc-current-font-weightvar(--font-weight-medium)
--toc-current-rail-colorvar(--color-primary)
--toc-depth0
--toc-indentvar(--space-5)
--toc-link-colorvar(--color-on-surface-muted)
--toc-link-font-familyvar(--font-secondary)
--toc-link-font-sizevar(--font-size-sm)
--toc-link-font-weightvar(--font-weight-normal)
--toc-link-padding-blockvar(--space-1)
--toc-link-padding-inlinevar(--space-3)
--toc-rail-colorvar(--color-border-subtle)
--toc-rail-width2px
--toc-guttervar(--space-6)
--toc-offsetvar(--scroll-padding, 6.25rem)
--toc-title-colorvar(--color-on-surface-muted)
--toc-title-font-familyvar(--font-primary)
--toc-title-font-sizevar(--font-size-xs)
--toc-title-font-weightvar(--font-weight-semibold)
--toc-title-letter-spacingnormal

Variants and states

Variants

  • .toc-sticky

Inside it

  • .toc-list
  • .toc-link
  • .toc-inline
  • .toc-title

State it reads

Read from the platform, never mirrored into a class that could disagree with it.

  • :focus
  • :hover

Before you ship it

This one needs a script. Opt the markup in with data-ui="toc" and import import 'mostlycss/js/components/toc' — that specifier and not the package root, which is a bundle carrying a second copy of the registry, so a page holding both initialises every element twice. What you lose without the script is listed below — for some components that is only polish, for others it is the whole interaction.

What you have to do 1 requirements

  1. Name the nav. aria-labelledby pointing at the visible "On this page" heading is the best version — the name is then on screen too — and aria-label="On this page" is correct when there is no visible title. A page with several <nav> landmarks and no names announces several identical ones. Every href must resolve to an id in the document. A link to a heading that is not there navigates nowhere, and the script skips it, so the section is silently missing from the highlight rather than visibly broken. Raise --scroll-padding if the page has a sticky header taller than 6.25rem. The reset sets scroll-padding-block-start: var(--scroll-padding) on html, which is what stops an in-page link landing underneath that header; the script reads the same value, so the heading marked as current is the one that lands in view.

src/css/components/toc.css · npx mostlycss add toc