Skip to main content
WireKit

On-Page TOC

The canonical wiring for an in-page TOC alongside long-form content — articles, docs pages, change logs, anything with five or more headings. WireKit ships two primitives that map onto the two common shapes:

  • Sidebar TOC (<x-wirekit::reading-spine>) — right-edge sticky vertical sidebar, hover-to-expand, narrow at rest. Matches Stripe, Linear, Vercel, shadcn docs. Recommended for docs / articles.
  • Horizontal strip TOC (<x-wirekit::reading-toc>) — sticky strip across the top of the viewport. Right shape for flat marketing pages with 3-4 anchored sections. See the marketing-landing-toc recipe.

This recipe covers the sidebar pattern.

The canonical wiring

Three primitives compose:

  1. <x-wirekit::reading-progress> — the top-edge progress bar (full-page, not scoped to article). Optional; drop if your layout doesn't want one.
  2. <x-wirekit::reading-spine> — the right-edge sidebar TOC. Reads <main> headings at the H2 / H3 levels. expand="hover" keeps the spine narrow at rest; expands to show labels on hover.
  3. <main> content — the spine reads headings from this element. Use target="main" (the prop default null resolves to 'main, article' first-match wins).
On-page TOC with sticky sidebar spine

CSS variables

The reading-spine reads these CSS variables; override in the wrapping element's style="…" or in :root {} for a global preset:

Variable Default Purpose
--reading-spine-width-collapsed 0.5rem Narrow-at-rest width (visible as a tick column)
--reading-spine-width-expanded 14rem Expanded width on hover / expand="always"
--reading-spine-offset-top 1rem boundary="container" only — pins the spine below the scroll-container top. Not used in this viewport-pinned recipe (the spine is vertically centered).
--reading-spine-padding-y 2rem Vertical padding inside the spine column
--color-wk-reading-spine-active var(--color-wk-accent) Active-link color

For the full token reference and theming guide, see /theming#reading-spine.

Integration with a sticky header

If your layout has a sticky header (e.g. <x-wirekit::brand-bar sticky>), pass the bar's height to the spine's :offset prop. The viewport-pinned spine is vertically centered, so it never sits behind the bar — but the IntersectionObserver fires when a heading crosses the viewport top (behind the bar), so without the offset the active link activates late. :offset moves that detection line down by the bar's height.

On-page TOC under a sticky brand-bar

Composing with a breadcrumb

If your layout also carries a <x-wirekit::breadcrumb>, the breadcrumb's active node and the spine's active link should reference the same logical position — both reflect "where the reader is". The breadcrumb is page-level (which page in the docs tree?); the spine is in-page (which section on the current page?).

The breadcrumb stays static for the whole page; the spine's active link updates as the reader scrolls.

On-page TOC with breadcrumb above the article

Tips

  • Use expand="always" on dedicated docs layouts (where readers expect the TOC) instead of the default expand="hover" — saves a hover-discovery moment.
  • Mobile users see no spine by default. The narrow-viewport experience drops the TOC; readers scroll the article without it. The progress bar at the top still works on mobile.
  • For flat content (3-4 sections, all <h2>), prefer the horizontal <x-wirekit::reading-toc> strip — see the marketing-landing-toc recipe.
  • For very long articles (50+ headings), the spine works but readers benefit from the <x-wirekit::reading-minimap> variant — a Reflexion of the article's structure as a vertical ruler.

See also