HomeProjectsScrollable Table Of Contents

Scrollable Table of Contents with Auto-Tracking

Published Aug 23, 2025
ā‹…
Updated Jan 29, 2026
ā‹…
3 minutes read

Scrollable Table of Contents with Auto-Tracking: Solving Sidebar Overflow

Internal Development Case Study

The Problem

The blog's table of contents sidebar had a fundamental usability issue: for lengthy posts with many headings, the fixed-height sidebar would overflow beyond the viewport. Users couldn't access all navigation links, and worse, when scrolling through the article, the highlighted active heading would disappear from view within the sidebar itself.

This created a frustrating experience where the navigation tool became less useful precisely when it was needed most-in long, complex articles with extensive heading hierarchies.

The Investigation

Testing revealed several UX pain points in the existing table of contents implementation:

The existing implementation used a fixed h-full height without any overflow handling, making it unusable for content-heavy posts.

The Solution: Intelligent Scrollable Navigation

Rather than simply adding scroll capability, we built a system that automatically maintains visibility of the active heading while providing smooth scrollable navigation.

Core Architecture Changes

Viewport-Constrained Height

max-h-[calc(100vh-12rem)] overflow-y-auto

React Ref for Sidebar Control

const navRef = useRef<HTMLElement>(null);

Heading Identification System

<button
  data-heading-id={heading.id}
  data-active={visibleHeadings.has(heading.id) ? "true" : "false"}
>
  {heading.text}
</button>

Auto-Scroll Implementation

The system tracks which headings are visible and automatically scrolls the sidebar to keep the active heading centered:

// Auto-scroll to keep active heading visible in sidebar
if (visibleSet.size > 0 && navRef.current) {
  const activeHeadingId = Array.from(visibleSet)[0];
  const activeButton = navRef.current.querySelector(
    `[data-heading-id="${activeHeadingId}"]`
  ) as HTMLElement;
  
  if (activeButton) {
    const navContainer = navRef.current;
    const navRect = navContainer.getBoundingClientRect();
    const buttonRect = activeButton.getBoundingClientRect();
    
    // Check if button is outside visible area
    if (buttonRect.top < navRect.top || buttonRect.bottom > navRect.bottom) {
      const scrollTop = activeButton.offsetTop - navContainer.offsetTop - 
        (navContainer.clientHeight / 2) + (activeButton.clientHeight / 2);
      navContainer.scrollTo({
        top: scrollTop,
        behavior: "smooth"
      });
    }
  }
}

Click-to-Center Enhancement

Manual navigation also benefits from intelligent positioning:

// Also scroll the sidebar to center the clicked heading
if (navRef.current) {
  const clickedButton = navRef.current.querySelector(
    `[data-heading-id="${id}"]`
  ) as HTMLElement;
  if (clickedButton) {
    const navContainer = navRef.current;
    const scrollTop = clickedButton.offsetTop - navContainer.offsetTop - 
      (navContainer.clientHeight / 2) + (clickedButton.clientHeight / 2);
    navContainer.scrollTo({
      top: scrollTop,
      behavior: "smooth"
    });
  }
}

Implementation Challenges

Intersection Observer Integration

The existing system used Intersection Observer to track visible headings. We extended this to trigger sidebar scrolling without interfering with the core functionality:

const handleIntersection = (entries: IntersectionObserverEntry[]) => {
  // Existing visibility tracking logic...
  setVisibleHeadings(new Set(visibleSet));
 
  // New: Auto-scroll sidebar to active heading
  if (visibleSet.size > 0 && navRef.current) {
    // Sidebar positioning logic...
  }
};

Smooth Animation Consistency

The solution needed to match the existing design system's animation timing. We used the same behavior: "smooth" approach as other scrolling interactions in the codebase.

Viewport Height Calculation

The max-h-[calc(100vh-12rem)] calculation accounts for:

Technical Outcomes

Performance

User Experience

Developer Experience

Edge Case Handling

Empty Visible Set

When no headings are visible (during rapid scrolling), the system gracefully does nothing rather than attempting invalid scrolls.

Multiple Visible Headings

The system prioritizes the first visible heading, providing consistent behavior when multiple sections are partially visible.

Boundary Conditions

Scroll calculations handle edge cases where the target heading is near the top or bottom of the sidebar container.

Future Enhancements

This foundation enables several potential improvements:

  1. Scroll Indicators: Visual hints showing scroll position within the sidebar
  2. Keyboard Navigation: Arrow key support for sidebar navigation
  3. Collapse/Expand: Hierarchical heading groups that can be folded
  4. Reading Progress: Integration with scroll progress indicators

Lessons Learned

Incremental Enhancement

Rather than rebuilding the entire table of contents, we enhanced the existing system with targeted improvements that preserved all working functionality.

User-Centered Problem Solving

The solution addressed the core user frustration (losing track of position) rather than just the technical symptom (sidebar overflow).

Integration Over Isolation

Working within the existing Intersection Observer system was more effective than creating a separate scrolling solution.

Sometimes the most impactful improvements are the ones that make existing features work better in edge cases that real users actually encounter.