On This Page

The "On this page" navigation provides a table of contents for long pages, allowing users to quickly jump to specific sections.

When to use

Use this pattern when:

  • A page has 3 or more distinct sections with headings
  • The content is longer than 2-3 screen heights
  • Users may want to navigate directly to specific information
  • The page contains reference or documentation content

When not to use

  • On short pages where all content is visible
  • On index or overview pages with card-based navigation
  • When there are fewer than 3 sections
  • On transactional pages (forms, checkout flows)

Structure

The component consists of:

  1. Title - "On this page" heading
  2. Link list - Anchor links to each section heading
  3. Active indicator - Highlights the current section while scrolling

Example

Placement

Position the "On this page" navigation in a sticky sidebar to the right of the main content. This keeps it accessible while scrolling without interfering with the primary reading flow.

  • Desktop (1200px+): Right sidebar, sticky position
  • Tablet/Mobile: Hidden or collapsed into a dropdown at the top of the content

Design Specifications

ElementPropertyValue
ContainerWidth220-280px
ContainerPositionsticky, top: var(--space-8)
TitleFont sizevar(--text-sm)
TitleText transformuppercase
TitleLetter spacing0.05em
TitleColourvar(--text-secondary)
ListBorder left2px solid var(--border-default)
LinkPaddingvar(--space-2) var(--space-3)
LinkColourvar(--text-secondary)
Link hoverColourvar(--text-primary)
Active linkColourvar(--color-primary)
Active linkBorder left2px solid var(--color-primary)
Active linkFont weightvar(--weight-medium)
Nested items (h3)Padding leftvar(--space-3) additional

Heading Levels

The navigation should reflect the document structure:

  • h2 headings - Top-level items, no indent
  • h3 headings - Nested items, indented under their parent h2
  • Deeper headings (h4+) are typically not included to keep the navigation concise

Scroll Behaviour

  • Use scroll-behavior: smooth for animated scrolling to anchors
  • Account for fixed headers with scroll-margin-top on target headings
  • Update the active indicator as the user scrolls using Intersection Observer
  • The first visible heading in the viewport should be highlighted

Accessibility

  • Wrap in <nav> with aria-label="On this page"
  • All links must have descriptive text matching the heading
  • Target headings must have id attributes for anchor linking
  • Focus is moved to the target heading when a link is clicked
  • Ensure keyboard navigation works correctly

HTML Structure

<nav class="on-this-page" aria-label="On this page">
  <h4 class="on-this-page-title">On this page</h4>
  <ul class="on-this-page-list">
    <li class="on-this-page-item">
      <a href="#section-1" class="on-this-page-link active">
        Section One
      </a>
    </li>
    <li class="on-this-page-item">
      <a href="#section-2" class="on-this-page-link">
        Section Two
      </a>
    </li>
    <li class="on-this-page-item nested">
      <a href="#subsection" class="on-this-page-link">
        Subsection (h3)
      </a>
    </li>
    <li class="on-this-page-item">
      <a href="#section-3" class="on-this-page-link">
        Section Three
      </a>
    </li>
  </ul>
</nav>

CSS Classes

.on-this-page {
  position: sticky;
  top: var(--space-8);
  max-height: calc(100vh - var(--space-16));
  overflow-y: auto;
  padding: var(--space-4);
  font-size: var(--text-sm);
}

.on-this-page-title {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 0 0 var(--space-3) 0;
}

.on-this-page-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-left: 2px solid var(--border-default);
}

.on-this-page-item {
  margin: 0;
}

.on-this-page-item.nested {
  padding-left: var(--space-3);
}

.on-this-page-link {
  display: block;
  padding: var(--space-2) var(--space-3);
  margin-left: -2px;
  border-left: 2px solid transparent;
  color: var(--text-secondary);
  text-decoration: none;
  transition: color 100ms ease, border-color 100ms ease;
}

.on-this-page-link:hover {
  color: var(--text-primary);
}

.on-this-page-link.active {
  color: var(--color-primary);
  border-left-color: var(--color-primary);
  font-weight: var(--weight-medium);
}

/* Hide on smaller screens */
@media (max-width: 1199px) {
  .on-this-page {
    display: none;
  }
}

JavaScript Behaviour

// Use Intersection Observer to track visible sections
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        // Update active link based on visible heading
        setActiveId(entry.target.id);
      }
    });
  },
  { rootMargin: '-80px 0px -80% 0px' }
);

// Observe all heading elements
document.querySelectorAll('h2, h3').forEach((heading) => {
  observer.observe(heading);
});