Scrollable Table of Contents with Auto-Tracking
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:
- Viewport Overflow: Long articles caused the sidebar to extend beyond screen bounds
- Hidden Active State: Current heading highlights scrolled out of sidebar view
- Manual Sidebar Navigation: Users had to manually scroll within the sidebar to find sections
- Inconsistent Visibility: No guarantee that the "current" heading indicator was actually visible
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-autoReact 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:
- Header space (top positioning)
- Bottom padding and margins
- Responsive layout considerations
Technical Outcomes
Performance
- Efficient Scrolling: Uses native
scrollTowith smooth behavior for optimal performance - Minimal Re-renders: Auto-scroll logic triggers only when visibility changes
- Intersection Observer Reuse: Leverages existing observation system without duplication
User Experience
- Always-Visible Active State: Current heading indicator never disappears from sidebar view
- Effortless Navigation: Users can scroll through long articles without losing their place in the TOC
- Predictable Behavior: Clicking any heading centers it in the sidebar view
- Responsive Design: Works across all screen sizes and content lengths
Developer Experience
- Minimal Code Changes: Solution integrated with existing component structure
- No Breaking Changes: All existing functionality preserved
- Type Safety: Full TypeScript support with proper ref typing
- Maintainable Logic: Clear separation between visibility tracking and scrolling behavior
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:
- Scroll Indicators: Visual hints showing scroll position within the sidebar
- Keyboard Navigation: Arrow key support for sidebar navigation
- Collapse/Expand: Hierarchical heading groups that can be folded
- 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.