Skip to main content

Catalog · Content

Table of contents

Lists the sections of a long article, beside the text when there is room and above it when there is not.

Fits best: blog — a long article, beside the text or above it

Example

The table of contents of a story, beside the text of the story.

Since March 2016, a group of volunteers has read the tide gauge in the port at six in the morning and six in the evening, every day of the year.

How the readings were taken

Each reading is the height of the water against the gauge, to the nearest centimetre, with the time, the wind and the state of the sea. Two people read it, and a third checks the sheet the same week.

Four centimetres in ten years

The average height at high tide was 4 cm higher in 2025 than in 2016. That is close to the rise that the national tide gauges record along the whole coast.

Storms that reach the walls

In the first five winters, waves reached the sea wall of the old town twice. In the last five, they reached it eleven times.

What the council plans next

The council will publish the full records as open data in January and has asked the volunteers to add a third daily reading.

Beside the text, at the end, when the article is 46rem wide or more; above it below that. With the catalog's script, the section in view is marked as you scroll.

Known issues

The mark on the section in view needs the catalog's script; without it, no link is marked. Beside the text, the contents sit at the inline end but come first in the source, so Tab reaches them before the text.

Not yet tested with a screen reader.

Code

Copy the markup

Bundler
import "@luz-ds/catalog/components/article.css";
HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@luz-ds/catalog@0.3.0/dist/components/article.min.css" integrity="sha384-++/qFKiq0czspViBwQZuh0EkOdjOJVbhRawyPCZDM6MqKb06/TXzqV1chdzP7Lt7" crossorigin="anonymous">
HTML
<article class="luz-article">
  <header class="luz-article-header">…</header>
  <div class="luz-article__layout">
    <nav class="luz-toc" aria-labelledby="toc-title" data-luz-spy>
      <h2 class="luz-toc__title" id="toc-title">Contents</h2>
      <ol class="luz-toc__list">
        <li><a href="#method">How the readings were taken</a></li>
        <li><a href="#level">Four centimetres in ten years</a></li>
        <li><a href="#storms">Storms that reach the walls</a></li>
      </ol>
    </nav>
    <div class="luz-prose">
      <p>Since March 2016, a group of volunteers has read the tide gauge…</p>
      <h2 id="method">How the readings were taken</h2>
      <p>…</p>
      <h2 id="level">Four centimetres in ten years</h2>
      <p>…</p>
      <h2 id="storms">Storms that reach the walls</h2>
      <p>…</p>
    </div>
  </div>
</article>

Variants and options

There are no modifiers. The contents are optional: without them, the text takes the first column alone.

Class or propertyWhat it does
.luz-article
The story: its width decides the layout, not the window's (a size container)
.luz-article__layout
The contents, then the text. When the article is 46rem wide or more: the text in the first column, the contents in a 13rem column at the inline end, 48 px apart; one column below that
.luz-toc
The <nav>. Beside the text it stays in view while the text scrolls, and a list taller than the window scrolls inside it
data-luz-spy on .luz-toc
The catalog's scroll spy marks the link of the section in view. Optional
.luz-toc__title
The heading of the contents, in the sans, semibold
.luz-toc__list
The list, with no markers and a rule along its inline start; one link a line
--luz-toc-top
The space above the contents while they stay in view: 24 px, or 24 px under a sticky bar (core's .o-sticky, one row of --luz-bar-h, such as the header). Set it on .luz-toc for a header of another height

How it works

A CSS grid inside a size container, from components/article.css of @luz-ds/catalog. No JavaScript of its own.

  • The layout follows the room the article has: in a page with a sidebar, or with larger text, the contents go above the text sooner. Below 46rem they are a plain list above the text, and do not stay in view: they would cover it.
  • The contents come first in the source, so they are read first and Tab reaches them first, in both layouts. The text is one element, the second child of __layout; in a CMS, the element that holds the post's content.
  • Each link points at the id of a section heading. Most CMSs can write those ids: in WordPress, the HTML anchor of each heading block.
  • With the catalog's script (catalog.js, or initSidebar() from @luz-ds/catalog/sidebar), the link of the section in view gets aria-current="true", which draws a bar and makes it semibold. The script finds the links by the data-luz-spy attribute on the <nav>: every link to a section of the page inside it.

Without JavaScript

The contents are a list of links to the sections, which the browser scrolls to. Only the mark on the section in view needs the script; without it, no link is marked.

Keyboard and ARIA

Tab moves through the links in order; Enter follows one. The browser moves focus and scrolls to the section.

  • A <nav> named by its heading: screen reader users find it in the list of landmarks as "Contents". Use an <ol>: the sections have an order.
  • aria-current="true" names the section in view; the script moves it. Without the script, nothing claims to be current.
  • Under a sticky header, the scroll padding of core's .o-sticky keeps the section's heading clear of the header when a link is followed.

Contrast and focus are in the Design tab.

Design

When to use

Use a table of contents in an article of four or more sections, which people read in parts or come back to: a guide, a report, a long interview.

When not to use

Do not use it in a short post: three paragraphs need no map.

  • Not for links to other pages: that is navigation, in the header or a sidebar.
  • Not for the steps of a task done in order: number the steps in the text.

Do and don't

Do Section headings that say what each part is about, so the list reads as a summary.
Don't Headings that could be in any article: the list tells the reader nothing.

Writing the content

Each link is the text of its section's heading, word for word, so the reader recognises where it leads.

  • List the <h2> of the article only. A list of more than about eight is a sign the article could be two.
  • Call it "Contents", in the reader's language ("Contenido" in Spanish).

Contrast and focus

The links are secondary text on the page; the current one is primary text with a bar in the brand colour.

PartColour on backgroundLightDarkHigh contrastNeeds
Title and current link
--luz-text-primary on --luz-surface-page
15.8:1
15.1:1
21.0:1
4.5:1
Link
--luz-text-secondary on --luz-surface-page
6.2:1
7.5:1
19.3:1
4.5:1
Link, hover
--luz-text-primary on --luz-surface-muted
15.8:1
15.1:1
20.1:1
4.5:1
Current link's bar
--luz-brand-primary on --luz-surface-page
5.6:1
7.7:1
14.1:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons.

  • Focus: each link keeps core's ring.
  • The current link is marked by the bar and the weight, not by colour alone. The links are not underlined: inside a <nav>, their place says they are links, as in every LUZ navigation.

Tokens it uses

The 15 tokens that the .luz-article and .luz-toc rules of catalog’s components/article.css read, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-space-2xl, --luz-space-2xs, --luz-space-lg, --luz-space-sm, --luz-space-xl, --luz-space-xs
Typography
--luz-font-sans, --luz-text-base, --luz-text-md

Keyboard and ARIA are in the Code tab.