Create Table of Contents Highlighting in React

Build an interactive navigation component that highlights the active section as users scroll, powered by the efficient IntersectionObserver API.

Why Table of Contents Highlighting Matters

A table of contents with highlighting is a powerful navigation component for long-form content. When users scroll through lengthy articles or documentation, an interactive TOC that highlights the active section helps them understand their position in the document and navigate quickly to other sections.

In React, building this feature is straightforward thanks to the IntersectionObserver API, which provides an efficient way to detect which elements are currently visible in the viewport.

This guide walks you through creating a complete table of contents component with active section highlighting, smooth scrolling, and sticky positioning. We'll cover multiple implementation approaches, from a basic version using React hooks to an optimized solution suitable for production applications.

Improving Content Discovery

When readers encounter lengthy articles or comprehensive guides, they often want to jump to specific sections rather than reading every word. A well-designed table of contents provides an at-a-glance overview of the document's structure, allowing users to find relevant information quickly. The highlighting feature adds another layer of usability by indicating which section the user is currently viewing, creating a sense of orientation within the content.

Modern web applications, from documentation sites to news portals, benefit significantly from this pattern. Visitors who use navigational aids like tables of contents tend to engage more deeply with content, viewing more pages and spending more time on the site overall.

Performance Considerations

Compared to older techniques that relied on binding scroll event handlers and performing calculations on every scroll event, the IntersectionObserver API offers a significantly more efficient approach. Scroll events fire dozens of times per second during scrolling, and attaching heavy calculations to each event can cause janky animations and decreased performance. The IntersectionObserver runs off the main thread for its calculations, providing smooth operation even on resource-constrained devices, as documented by the MDN Intersection Observer API specifications.

Accessibility Benefits

An interactive table of contents serves as a secondary navigation mechanism that benefits users who rely on keyboard navigation or screen readers. By providing clear, semantic links to document sections, you make your content more accessible to a wider audience. The highlighting mechanism should also communicate its state to assistive technologies through appropriate ARIA attributes.

Setting Up the IntersectionObserver
1useEffect(() => {2 const observer = new IntersectionObserver(3 (entries) => {4 entries.forEach(entry => {5 if (entry.isIntersecting) {6 onActiveChange(entry.target.id);7 }8 });9 },10 {11 rootMargin: '-10% 0px -80% 0px',12 threshold: 013 }14 );15 16 headings.forEach(heading => {17 const element = document.getElementById(heading.id);18 if (element) observer.observe(element);19 });20 21 return () => observer.disconnect();22}, [headings, onActiveChange]);

Building the Table of Contents Component

Step 1: Extracting Headings

Before you can highlight sections, you need to know what sections exist. The first step is to extract all headings from your content:

const extractHeadings = (contentRef) => {
 const headings = [];
 const elements = contentRef.current.querySelectorAll('h2, h3');

 elements.forEach((heading, index) => {
 const id = heading.id || `section-${index}`;
 heading.id = id;
 headings.push({
 id,
 text: heading.textContent,
 level: heading.tagName.toLowerCase()
 });
 });

 return headings;
};

This function queries the content container for heading elements, assigns unique IDs if they don't exist, and builds an array of heading metadata. The IDs are essential for creating anchor links that the table of contents can reference.

Step 2: Creating the TOC Structure

The table of contents component renders as a navigation element with links to each heading:

const TableOfContents = ({ headings, activeId, onNavigate }) => {
 return (
 <nav className="toc" aria-label="Table of contents">
 <ul>
 {headings.map(heading => (
 <li
 key={heading.id}
 className={`toc-item toc-item-${heading.level} ${
 activeId === heading.id ? 'active' : ''
 }`}
 >
 <a
 href={`#${heading.id}`}
 onClick={(e) => onNavigate(e, heading.id)}
 aria-current={activeId === heading.id ? 'location' : undefined}
 >
 {heading.text}
 </a>
 </li>
 ))}
 </ul>
 </nav>
 );
};

The component receives the headings array, the currently active section ID, and a navigation callback. Each link's aria-current attribute helps assistive technologies understand the current state.

Step 3: Smooth Scrolling

When users click a TOC link, implement smooth scrolling:

const handleNavigate = (e, id) => {
 e.preventDefault();
 const element = document.getElementById(id);
 element.scrollIntoView({ behavior: 'smooth', block: 'start' });
};

The scrollIntoView method with behavior: 'smooth' creates a pleasing animation. The block: 'start' alignment ensures the heading appears at the top of the viewport.

Step 4: Complete Implementation

Putting it all together, your complete TOC component should handle heading extraction, observer setup, and navigation. For production applications built with Next.js, consider using a custom hook pattern that encapsulates all the logic:

const useTableOfContents = (contentRef) => {
 const [headings, setHeadings] = useState([]);
 const [activeId, setActiveId] = useState('');

 useEffect(() => {
 const extractedHeadings = extractHeadings(contentRef);
 setHeadings(extractedHeadings);

 const observer = new IntersectionObserver(
 (entries) => {
 entries.forEach(entry => {
 if (entry.isIntersecting) {
 setActiveId(entry.target.id);
 }
 });
 },
 { rootMargin: '-10% 0px -80% 0px', threshold: 0 }
 );

 extractedHeadings.forEach(heading => {
 const element = document.getElementById(heading.id);
 if (element) observer.observe(element);
 });

 return () => observer.disconnect();
 }, [contentRef]);

 return { headings, activeId };
};
Key Features of an Effective Table of Contents

Active Section Highlighting

Visually indicates which section the user is currently viewing using the IntersectionObserver API.

Sticky Positioning

Keeps the navigation visible as users scroll through long content with CSS position: sticky.

Smooth Scrolling

Creates a pleasing navigation experience when users click TOC links to jump between sections.

Accessibility Support

Uses semantic HTML and ARIA attributes to ensure the TOC works with screen readers and keyboard navigation.

CSS Styling for Table of Contents

Sticky Positioning

.toc {
 position: sticky;
 top: 2rem;
 max-height: calc(100vh - 4rem);
 overflow-y: auto;
}

The max-height ensures the navigation doesn't extend beyond the viewport, and overflow-y: auto adds scrollbars when there are many sections.

Active State Styling

.toc-item.active a {
 color: var(--accent-color);
 border-left-color: var(--accent-color);
 font-weight: 600;
}

.toc-item a {
 border-left: 3px solid transparent;
 padding-left: 1rem;
 transition: all 0.2s ease;
}

These styles create a clear visual indicator of the active section using both color and a left border, with smooth transitions between states. The border-left approach provides a visual hierarchy that works well even for nested heading levels.

Responsive Design

On smaller screens, the table of contents often needs to move from a sidebar position to a collapsible menu or section at the top of the content. Consider using a CSS media query to hide the sidebar TOC and show a hamburger menu instead:

@media (max-width: 768px) {
 .toc-sidebar {
 display: none;
 }
 
 .toc-mobile-toggle {
 display: block;
 }
}

Visual Feedback Patterns

Beyond basic highlighting, consider adding subtle animations that indicate scroll direction. For example, you could animate a connecting line between TOC items to show the user's progression through the content, similar to patterns used in React component libraries for documentation sites.

Advanced Implementation Patterns

Handling Server-Side Rendering

When using React with server-side rendering frameworks like Next.js, you need to handle the case where the DOM isn't available during the initial render. The IntersectionObserver setup should be wrapped in a useEffect or a client-side only check to prevent server-side errors:

const TableOfContents = ({ contentRef }) => {
 const [headings, setHeadings] = useState([]);
 const [activeId, setActiveId] = useState('');
 const [isClient, setIsClient] = useState(false);

 useEffect(() => {
 setIsClient(true);
 if (contentRef.current) {
 const extracted = extractHeadings(contentRef);
 setHeadings(extracted);
 }
 }, [contentRef]);

 // Observer setup...

 if (!isClient) return null;

 return <nav>...</nav>;
};

Managing Scroll Position

If users navigate directly to a section via URL hash, you might want to highlight that section immediately. You can read the current hash from window.location and set it as the active section when the component mounts.

Dynamic Content Handling

If your content loads dynamically or uses client-side routing, you'll need to re-extract headings and re-attach observers when the content changes. Use React's dependency arrays carefully to trigger updates when needed. The React Hydration guide covers similar patterns for handling client-side content updates.

Performance Monitoring

For large-scale applications, consider monitoring the performance of your TOC implementation using the browser's Performance API. Track how long the IntersectionObserver callback takes and optimize accordingly. When building complex React applications, these small optimizations add up across the entire user experience.

Frequently Asked Questions

How does IntersectionObserver improve performance?

The IntersectionObserver API runs its calculations off the main thread, unlike scroll event handlers that fire on every scroll action. This means your highlighting logic won't block the main thread or cause janky animations during scrolling. According to [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API), the API is specifically designed for efficient viewport detection.

What threshold value should I use?

A threshold of 0 with a rootMargin that creates a narrow observation zone (like '-10% 0px -80% 0px') works well for most TOC implementations. This ensures sections are highlighted as they enter a specific area of the viewport. The [CSS-Tricks guide](https://css-tricks.com/table-of-contents-with-intersectionobserver/) demonstrates various threshold configurations for different use cases.

How do I handle dynamic content?

If your content loads dynamically or uses client-side routing, you'll need to re-extract headings and re-attach observers when the content changes. Use React's dependency arrays in useEffect to trigger updates appropriately. Consider using a custom hook that watches for content changes and rebuilds the TOC.

Can I use this with server-side rendering?

Yes, but wrap the IntersectionObserver setup in a useEffect or add a client-side check to prevent server-side errors. The DOM isn't available during server rendering, so the observer can't be set up until the component mounts on the client. See the Next.js documentation for patterns on handling browser-only APIs.

Build Better React Applications

Need help implementing interactive features like table of contents in your React application? Our team specializes in building performant, accessible React applications that deliver exceptional user experiences.

Sources

  1. LogRocket: Create a table of contents with highlighting in React - Comprehensive tutorial covering IntersectionObserver API for scrollspy functionality with complete code examples
  2. CSS-Tricks: Table of Contents with IntersectionObserver - Overview of modern approaches including Ben Frain's implementation and various CodePen demos
  3. Ben Frain: Building a table of contents with active indicator - Scrollspy algorithm and performance optimization techniques
  4. MDN: Intersection Observer API - Official browser API documentation