Toc

A column of links to the headings on the page, with the one being read marked as the reader scrolls.

Example

View Code
<nav class="toc" aria-label="On this page">
    <ul role="list">
        <li><a href="#">Getting the files</a></li>
        <li><a href="#" aria-current="true">The stylesheet</a></li>
        <li><a href="#">A module</a></li>
        <li><a href="#">The vocabulary</a></li>
        <li><a href="#">A first layout</a></li>
        <li><a href="#">Editor completion</a></li>
        <li><a href="#">Going further</a></li>
    </ul>
</nav>

When to use it

A long page a reader scrolls rather than clicks through: a guide, a reference, a post with sections. The list says what is on the page and how far through it the reader is. A page with three short sections does not need one, and neither does a page whose headings would not fit down one side.

How it works

A column of links, each pointing at the id of a heading on the same page. The current one carries aria-current="true" and takes the variant's color, a strong weight, and a bar down its start edge; the bar is on every link and transparent until then, so nothing shifts as the mark moves. data-variant picks the hue and data-size steps the text. A link under the pointer takes --yeti-toc-hover, read with a fallback to the variant's own subtle tint, so setting it to transparent turns the fill off without touching the variant.

toc.js moves the mark. It watches the linked headings with one IntersectionObserver and marks the link of whichever heading is topmost in view, dispatching yeti:current on the .toc with the link and the heading each time it moves. Between two headings nothing is in view, and the last mark stays rather than flickering off. Without the module the list is a working list of links, and a page that knows its own current section can simply ship the attribute in its HTML.

Following a link scrolls smoothly, from --yeti-toc-scroll applied to the page — the token collapses to auto under prefers-reduced-motion, which a plain scroll-behavior in this layer could not, since it would outrank the reset's own rule.

Each row's block padding is --yeti-toc-padding, a control's density by default; set it smaller on one .toc for a book's own contents page.

The list may itself be a cluster instead of a plain ul, for a row of contents rather than a column — a settings page's section list beside its narrow rail, say. The toc's own column and gap only apply to a list that is not a cluster, so the layout class wins and the row wraps as any cluster does.

data-numbered turns the list into a contents list. Every ul resets a CSS counter and every li counts, so a nested list picks up where its parent left off and reads 2.1 rather than starting over at 1. The number sits before the link, in the muted color, with tabular figures, in a box wide enough for a two-figure count or a nested 2.1, so the link text lines up. A title that wraps hangs from the number: its second line starts under its first word, not under the digits.

<nav class="toc" aria-label="Contents" data-numbered>
    <ul role="list">
        <li><a href="#one">Getting the files</a></li>
        <li><a href="#two">The stylesheet</a>
            <ul role="list"><li><a href="#linking">Linking it</a></li></ul>
        </li>
    </ul>
</nav>
<nav class="toc" aria-label="On this page" data-variant="secondary" data-size="sm">
    <ul role="list">
        <li><a href="#install">Install</a></li>
        <li><a href="#usage">Usage</a></li>
    </ul>
</nav>

Accessibility

Label the nav — aria-label="On this page" — because a page with a toc has at least two navs and they have to be told apart. Put role="list" on the ul, which is what keeps it a list where the reset removes the markers. The current section's link carries aria-current="true"; the color and the bar are the visible half of the same fact, never the only half. The module owns that attribute on these links: it sets and clears aria-current="true" and strips any other aria-current value an author puts on a toc link. The links are ordinary same-page links, so the keyboard reaches them in order and Enter follows one with no script at all.

Attributes

Attribute Type Values Default Description
data-variant enum primary, secondary, success, warning, alert, danger, neutral, black, white primary The hue of the current link and its edge bar.
data-size enum sm, md, lg md The text step, and the inset of each link with it.
data-numbered boolean Number the entries before their links, nested ones as 2.1, in the muted color.

Children

  • > ul: exactly 1. The links, one per li, with role="list".
  • li: at least 1. One entry, holding a link whose href is the id of a heading on this page.

Tokens

Token Description
--yeti-toc-scroll How the page scrolls to a heading; also read by a nav with in-page links.
--yeti-toc-padding The rows' block padding.
--yeti-color-text-muted A link that is not the current one.
--yeti-color-text A link under the pointer.
--yeti-toc-hover The hover fill; transparent turns it off.
--yeti-space-xs Gap between links, and their block padding.
--yeti-space-sm The space step when data-size is absent.
--yeti-text-md Text size when data-size is absent.
--yeti-border-width The edge bar is three of them.
--yeti-weight-strong Weight of the current link.
--yeti-duration-fast How long the mark takes to move.
--yeti-ease The curve of that transition.
--yeti-color-primary The default variant's color, when data-variant is absent.
--yeti-color-primary-subtle The default variant's hover tint.
--yeti-color-primary-text The default variant's text color.
Internal tokens (may change between minor versions) - `--_yeti-variant` - `--_yeti-variant-subtle` - `--_yeti-variant-text` - `--_yeti-size-text` - `--_yeti-size-space`

Accessibility

  • Required attributes: aria-label or aria-labelledby
  • Give the nav an aria-label such as "On this page", since a page with a toc has at least two navs. Put role="list" on the ul, because the reset only removes list markers where that role says the list is decorative. The link for the section being read carries aria-current="true"; toc.js moves it, and a page that ships the mark in its HTML is correct without the module. The module owns aria-current on these links: it sets and clears the value "true" and strips any other aria-current an author puts on a toc link. Every link points at an id on this page, so the list works with no script at all.

Browser support

  • Used without guards: IntersectionObserver, :has(), scroll-behavior
  • Behind @supports: nothing

JavaScript

Optional enhancement: components/toc/toc.js. The component works without it.

Each event bubbles, crosses a shadow boundary, and cannot be cancelled.

Event Module Detail Description
yeti:current toc.js { link, heading } Dispatched on the .toc when the mark moves to another link.

Available since 7.0.0.