Table of contents
Table of contents — the "on this page" list beside an article.
Examples
<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.
<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.
| Token | Default |
|---|---|
| --toc-current-color | var(--color-primary) |
| --toc-current-font-weight | var(--font-weight-medium) |
| --toc-current-rail-color | var(--color-primary) |
| --toc-depth | 0 |
| --toc-indent | var(--space-5) |
| --toc-link-color | var(--color-on-surface-muted) |
| --toc-link-font-family | var(--font-secondary) |
| --toc-link-font-size | var(--font-size-sm) |
| --toc-link-font-weight | var(--font-weight-normal) |
| --toc-link-padding-block | var(--space-1) |
| --toc-link-padding-inline | var(--space-3) |
| --toc-rail-color | var(--color-border-subtle) |
| --toc-rail-width | 2px |
| --toc-gutter | var(--space-6) |
| --toc-offset | var(--scroll-padding, 6.25rem) |
| --toc-title-color | var(--color-on-surface-muted) |
| --toc-title-font-family | var(--font-primary) |
| --toc-title-font-size | var(--font-size-xs) |
| --toc-title-font-weight | var(--font-weight-semibold) |
| --toc-title-letter-spacing | normal |
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
- Name the nav.
aria-labelledbypointing at the visible "On this page" heading is the best version — the name is then on screen too — andaria-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. Everyhrefmust resolve to anidin 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-paddingif the page has a sticky header taller than 6.25rem. The reset setsscroll-padding-block-start: var(--scroll-padding)onhtml, 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