v1.6.x-dev NDS IQ v6

TOC - National Design System

A navigable outline that auto-builds from a page's headings and keeps the reader's current section highlighted as they scroll.

Auto-Populated

Point the TOC at an article with data-toc-source and it builds the list from the headings it finds. Missing heading IDs get slugified automatically so anchors resolve.

Built from #tocSampleArticle h2/h3/h4 headings

Introduction

Overview paragraph describing the page purpose.

Setup

Preparing the environment and installing dependencies.

Requirements

What you need before you begin.

Install

Running the install command.

macOS

Notes for macOS users.

Windows

Notes for Windows users.

Usage

Typical usage patterns.

Support

Where to go for help.

<nav class="nds-toc" aria-label="Table of contents" data-toc-source="#articleRoot" style="--toc-skeleton-rows: 8"> <div class="nds-toc-head"> <span class="nds-label">On this page</span> <h2 class="nds-toc-title nds-truncate">Page Title</h2> </div> <div class="nds-drawer nds-lined"> <ul class="nds-drawer-list"></ul> </div> </nav>

Manual Markup

Author the list yourself when the TOC doesn't mirror a page's headings (custom labels, filtered entries, non-heading anchors). Nest <ul> inside an <li> for any number of sub-levels.

Three-level TOC, written by hand
<nav class="nds-toc" aria-label="Table of contents"> <div class="nds-toc-head"> <span class="nds-label">On this page</span> <h2 class="nds-toc-title nds-truncate">Page Title</h2> </div> <div class="nds-drawer nds-lined"> <ul class="nds-drawer-list"> <li> <a href="#manual-section-1" class="nds-btn nds-subtle nds-indicator"> <span class="nds-label nds-truncate">Section 1</span> </a> </li> <li> <a href="#manual-section-2" class="nds-btn nds-subtle nds-indicator"> <span class="nds-label nds-truncate">Section 2</span> </a> <ul> <li> <a href="#manual-section-2a" class="nds-btn nds-subtle nds-indicator"> <span class="nds-label nds-truncate">Sub A</span> </a> <ul> <li> <a href="#manual-section-2a1" class="nds-btn nds-subtle nds-indicator"> <span class="nds-label nds-truncate">Detail one</span> </a> </li> <li> <a href="#manual-section-2a2" class="nds-btn nds-subtle nds-indicator"> <span class="nds-label nds-truncate">Detail two</span> </a> </li> </ul> </li> <li> <a href="#manual-section-2b" class="nds-btn nds-subtle nds-indicator"> <span class="nds-label nds-truncate">Sub B</span> </a> </li> </ul> </li> <li> <a href="#manual-section-3" class="nds-btn nds-subtle nds-indicator"> <span class="nds-label nds-truncate">Section 3</span> </a> </li> </ul> </div> </nav>

Built-in Features

Auto-initialization

Activates on any .nds-toc element on the page. No manual wiring required.

Heading-Driven List

Scans the article you point it at and builds the full nested list, slugifying any heading that lacks an id.

Unlimited Depth

Each sub-level picks up its own indent and side rail, so deeply nested sections read clearly without extra markup.

Active-Section Tracking

Highlights the section currently below the sticky nav as the reader scrolls, with the indicator following in real time.

Click-to-Scroll

Clicking a TOC entry smooth-scrolls the target heading into view beneath the nav, updates the URL hash, and respects reduced-motion preferences.

Programmatic Control

Create a single instance or reinitialize all TOCs after injecting new content through the NDS.Toc API.

Usage Guidelines

Live Example

Best Practices

  • Use a TOC on long-form content (policy pages, documentation, guides) where readers benefit from skimming the structure and jumping around
  • Use auto-populate (data-toc-source) whenever the TOC should mirror the article one-to-one. It stays in sync automatically as headings are added, renamed, or removed
  • Use manual markup only when you need labels that differ from the headings, a filtered subset, or anchors that aren't headings
  • Do not place a TOC on short pages where every section is already visible. Use the Drawer for plain navigation or the Stepper for linear multi-step flows instead
  • Place the TOC inside a sideinfo column with nds-sticky so it stays visible as the reader scrolls long content
  • Pick nds-sm or nds-md on the surrounding .nds-sideinfo for compact rails. The default width is tuned for richer sideinfo content, not link lists
  • Keep the TOC to three levels or fewer. Deeper trees produce tight indents that are hard to scan and hint at a page that should be split
  • Set data-toc-levels="h2,h3" to skip h4s if the article uses them for inline emphasis rather than real sub-sections
  • Give every heading a stable, human-readable id. The auto-slugifier is a fallback, not a substitute for author-chosen anchors
  • Author labels should match the heading text. Invent TOC-only names only when the heading is verbose and the rail cannot truncate cleanly
  • Set --toc-skeleton-rows on .nds-toc to the page's link count — the still-empty list then reserves the exact height, so content below the TOC (the whole article on stacked mobile layouts) does not shift when the list fills in

Data Attributes

AttributeDescription
data-toc-sourceCSS selector for the container whose headings should populate the list. Omit for manual markup.
data-toc-levelsComma-separated heading tags to include (default: h2,h3,h4). Use h2 for a flat TOC or h2,h3,h4,h5 for deeper docs.
data-nds-toc-initializedStamped true after a TOC instance is created — also when the source yields no headings, so the height reservation always releases. Checked by reinit() to prevent double-initialization. Removed by destroy().

Modifier Classes

ClassApplied toDescription
nds-lined.nds-drawerRequired. Renders the vertical rail beside sub-lists. .nds-toc sets --drawer-lined-block: 0px internally so the rail extends flush to the block edges of each nested list.

CSS Custom Properties

PropertyDefaultDescription
--toc-skeleton-rows6Expected link count for an auto-populated TOC. Until JS fills the list, the empty list reserves rows × 36px so the content below does not shift when the list lands. Set it on .nds-toc to the page's real link count for a shift-free load.
--drawer-lined-block0pxBlock-axis inset of the lined rail on the drawer. Set to 0px by .nds-toc so the rail extends flush to the top and bottom of each nested list.
--drawer-lined-width2pxThickness of the lined rail. Set to 2px by .nds-toc for a slimmer rail than the drawer default.
--drawer-indicator-width3pxWidth of the active-item side indicator. Set to 3px on .nds-toc .nds-drawer for a bolder highlight than the base drawer default.

Loading and Skeleton State

When explicitly set to loading (nds-loading class or data-state="loading"), item labels animate as skeleton bars — the same contract as tables, tabs, and accordion. Before JS initializes an auto-populated TOC, the still-empty list reserves its expected height, sized by --toc-skeleton-rows, so the content below it does not shift when the list fills in.

JavaScript API

The NDS.Toc API initializes, re-initializes, and creates TOC instances. Auto-init runs on DOMContentLoaded; call NDS.Toc.reinit() after injecting new TOC markup dynamically.

// ── Initialize all TOCs on the page ───────────────── // Called automatically once. Re-run after injecting new TOC markup. NDS.Toc.init(); NDS.Toc.reinit(); // ── Create a single TOC instance ──────────────────── // Returns the NDSToc instance (with .active, .entries, .destroy(), etc.) const toc = document.querySelector('.nds-toc'); const instance = NDS.Toc.create(toc); // ── Read the currently-active entry ────────────────── instance.active; // { link, li, target } | null instance.entries; // Array of { link, li, target } // ── Manually tear down and re-wire ────────────────── instance.destroy(); // Remove click + scroll listeners, clear state instance.update(); // Recompute active entry from current scroll
Last Modified Date: 02/08/2026 - 11:19 PM
Was this page useful?
60% of users said Yes from 2843 Feedbacks