Create a table of contents with highlighting in React
Published: 2024-01-19 03:02:17

A table of contents summarizes the page's content, allowing site visitors to swiftly move to portions of the page by clicking on the desired heading. Tables of contents are commonly used in manuals and blogs. This component will dynamically render a list of page headings and highlight which heading you are currently viewing.

In this article, we'll look at how to make a sticky table of contents that dynamically lists the available headings on a page, emphasizing the active headings. As we read our article, when a heading appears on the screen, it will be highlighted in the TOC, as shown in the animation below:

## prerequisite

- Node.js installed on your system.

- Basic understanding of react js.

## Setting up React

- Open a terminal(Windows Command Prompt or PowerShell).

- Create a new project folder: mkdir ReactProjects and enter that directory: cd ReactProjects.

- Install React using create-react-app, a tool that installs all of the dependencies to build and run a full React.js application:

`npx create-react-app my-app 

npm start // to start react
`
## Create a Table of Content Component (TOC) 
Our TOC component will be placed on the right side of our screen, so let's start by designing it. 
first create a` TableOfContent.js `file and a `tableOfContent.css` file in the src directory. Add the following lines of code to the `TableOfContent.js` file:


```javascript
// src/TableOfContent.js
import './tableOfContent.css'

function TableOfContent() {
  return (
    <nav>
      <ul>
        <li>
          <a href='#'>A heading</a>
        </li>
      </ul>
    </nav>
  )
}
export default TableOfContent
```
From code above we imports a CSS file named `tableOfContent.css`. It defines a React functional component called TableOfContent that renders a navigation bar `<nav>` with an unordered list `<ul>` containing a single list item `<li>` with a placeholder anchor `<a>` link to "A heading" (represented by '#'). The component is exported for use in other files.

Next, add the following lines of code in the `tableOfContent.css` file:

```css
// src/tableOfContent.css
nav {
  width: 220px;
  min-width: 220px;
  padding: 16px;
  align-self: flex-start;
  position: -webkit-sticky;
  position: sticky;
  top: 48px;
  max-height: calc(100vh - 70px);
  overflow: auto;
  margin-top: 150px;
}

nav ul li {
  margin-bottom: 15px;
}

```
To display this component, navigate to the App.js file and insert the following import:

```javascript
import TableOfContent from './TableOfContent';
```
Next, modify the `App` component to look like the following:

```javascript
// src/App.js
function App() {
  return (
    <div className="wrapper">
      <Content />
      <TableOfContent />
    </div>
  );
}
```
With the code above, we'll see a sticky component on the right side of our app.

## Find the headings on the page
We can use the `querySelectorAll` document method to locate all of the headers on our page, which returns a NodeList containing a list of items that match the supplied collection of selectors.

The following example demonstrates how to utilize the `querySelectorAll` method:


```javascript
const headings = document.querySelectorAll(h2, h3, h4);
```
The code above will select all <h2>, <h3>, and <h4> elements in the DOM and store them in the headings variable as a NodeList, which is similar to an array and contains references to the selected elements.

Now, in the `TableOfContent.js` file, add the following import to find the headings:

```javascript
import { useEffect, useState } from 'react';
```
Next, in the component, add the following lines of code before the return statement:

```javascript
// src/TableOfContent.js
const [headings, setHeadings] = useState([])

useEffect(() => {
  const elements = Array.from(document.querySelectorAll("h2, h3, h4"))
    .map((elem) => ({
      text: elem.innerText,
    }))
  setHeadings(elements)
}, [])
```
In the given code above, the React functional component TableOfContent uses the useState and useEffect hooks. It initializes a state variable headings as an empty array and sets it using setHeadings within the useEffect hook. The useEffect hook runs once when the component mounts (empty dependency array []). It selects all <h2>, <h3>, and <h4> elements from the DOM, converts them to an array, extracts their inner text, and stores the resulting array of objects with text content in the headings state variable.

Now, to display the headings in the TOC, modify the return statement of the component to look like the following code:

```javascript
// src/TableOfContent.js
return (
  <nav>
    <ul>
      {headings.map(heading => (
        <li key={heading.text}>
          <a href='#'>{heading.text}</a>
        &lt;/li>
      ))}
    </ul>
  </nav>

```
The code above returns a table of contents within a navigation (<nav>) element, using the headings state. It maps through the headings array, creating list items (<li>) for each heading. Each list item contains an anchor (<a>) link with the heading text, represented by heading.text. The key attribute is set to the heading text to provide a unique identifier for each list item.

## Link and listing heading in hierarchy
Headings do the important work of showing the structure of digital content. They help all users understand and navigate your digital content more easily, including those with disabilities.

When we click on a heading in the TOC, we are not taken to the appropriate section. You'll see that they're all in the same line, with no indication of which is the main heading and which is a subheading. Let's get things sorted out.

In the TableOfContent component, modify the useEffect Hook to look like the following code:

```javascript
// src/TableOfContent.js
useEffect(() => {
  const elements = Array.from(document.querySelectorAll("h2, h3, h4"))
    .map((elem) => ({
      id: elem.id,
      text: elem.innerText,
      level: Number(elem.nodeName.charAt(1))
    }))
  setHeadings(elements)
}, [])
```
In addition to the text from the headings, we are adding an ID and a level property to the state. We'll provide the ID to the TOC text's anchor tag so that when we click on it, we'll be directed to the appropriate portion of the page. The level property will then be used to establish a hierarchy in the TOC.

Modify the `ul` element in the TableOfContent component's return statement to look like this:

```javascript
// src/TableOfContent.js
<ul>
  {headings.map(heading => (
    <li
      key={heading.id}
      className={getClassName(heading.level)}
      >
      <a
        href={`#${heading.id}`}
        onClick={(e) => {
          e.preventDefault()
          document.querySelector(`#${heading.id}`).scrollIntoView({
            behavior: "smooth"
          })}}
        >
        {heading.text}
      </a>
    </li>
  ))}
</ul>
```
The code above returns an unordered list `<ul>` representing a dynamic table of contents. It maps through the headings array, creating list items `<li>` for each heading. Each list item contains an anchor `<a>` link with the heading text, which, when clicked, smoothly scrolls to the corresponding element with the specified heading.id in the document.

The key attribute is set to heading.id to provide a unique identifier for each list item. The className attribute is set using the **getClassName** function, which assigns a class based on the** heading.level** value.

Next, to create the **getClassName** function, add the following code outside the TableOfContent component:

```javascript
// src/TableOfContent.js
const getClassName = (level) => {
  switch (level) {
    case 2:
      return 'head2'
    case 3:
      return 'head3'
    case 4:
      return 'head4'
    default:
      return null
  }
}
```

The `getClassName` function takes a level parameter and returns a corresponding class name based on the level value. If level is 2, it returns 'head2'; if 3, it returns 'head3'; if 4, it returns 'head4'; otherwise, it returns null.

Now, add the following lines of code in the in tableOfContent.css file:

```css
// src/tableOfContent.css
.head3{
  margin-left: 10px;
  list-style-type: circle;
}
.head4{
  margin-left: 20px;
  list-style-type: square;
}
```
## Observing active headings with the Intersection Observer API

API to detect the visibility allows us to monitor a target element and execute a function when it reaches a predefined point.

Using the Intersection Observer API, we'll write a custom Hook that returns the active header's ID. The ID returned will then be used to highlight the matching text in our TOC.

To do so, create a hook.js file in the src directory and add the following lines of code:

```javascript
// src/hooks.js
import { useEffect, useState, useRef } from 'react';

export function useHeadsObserver() {
  const observer = useRef()
  const [activeId, setActiveId] = useState('')

  useEffect(() => {
    const handleObsever = (entries) => {}

    observer.current = new IntersectionObserver(handleObsever, {
      rootMargin: "-20% 0% -35% 0px"}
    )

    return () => observer.current?.disconnect()
  }, [])

  return {activeId}
}
```

ChatGPT
The code above defines a custom React hook useHeadsObserver, which uses IntersectionObserver to observe elements on the page. It initializes observer and activeId as a ref and state variable, respectively, using useRef and useState.

The useEffect hook sets up the IntersectionObserver with handleObserver as the callback function. The observer is configured to have a rootMargin of "-20% 0% -35% 0px", which adjusts the observation area. The returned function disconnects the observer when the component unmounts.

Finally, the hook returns an object with the activeId, which represents the ID of the currently intersecting element (the element currently in the viewport or within the specified rootMargin area).

Let's specify the heads we wish to monitor by supplying them to the Intersection Observer's observe method. We'll also change the handleObsever callback method to save the intersected header's ID in the state.

To do so, modify the useEffect Hook to look like the code below:

```javascript
// src/hooks.js
useEffect(() => {
  const handleObsever = (entries) => {
    entries.forEach((entry) => {
      if (entry?.isIntersecting) {
        setActiveId(entry.target.id)
      }
    })
  }

  observer.current = new IntersectionObserver(handleObsever, {
    rootMargin: "-20% 0% -35% 0px"}
  )

  const elements = document.querySelectorAll("h2, h3", "h4")
  elements.forEach((elem) => observer.current.observe(elem))
  return () => observer.current?.disconnect()
}, [])

```
looking at the code above, useEffect hook sets up an IntersectionObserver to observe specified elements on the page (h2, h3, and h4). When an observed element becomes intersecting with the viewport or within the specified rootMargin, its ID is set as the activeId state variable. The observer is disconnected when the component unmounts.

In the TableOfContent.js file, import the created Hook with the following code:

```javascript
// src/TableOfContent.js
import { useHeadsObserver } from './hooks'
```
Now, call the Hook after the headings state in the TableOfContent component:

```javascript
// src/TableOfContent.js
const {activeId} = useHeadsObserver()
```
When a heading element crosses with the code above, it will be available with activeId.

## Highlighting the active heading
To highlight the active headings in our TOC, modify the anchor tag <a></a> of the li element in the returned statement of the TableOfContent component by adding the following style attribute:

```css
style={{
  fontWeight: activeId === heading.id ? "bold" : "normal" 
}}
```
Now, our anchor tag will look like this:

```javascript
// src/TableOfContent.js
<a
  href={`#${heading.id}`} 
  onClick={(e) => {
    e.preventDefault()
    document.querySelector(`#${heading.id}`).scrollIntoView({
      behavior: "smooth"
    })}}
    style={{
      fontWeight: activeId === heading.id ? "bold" : "normal" 
    }}
  >
  {heading.text}
</a>
```
from the code above we creates an anchor `<a> `element for each heading, where heading.id is used in the href attribute. When clicked, it smoothly scrolls to the corresponding element. The style attribute sets the fontWeight to "bold" if activeId matches heading.id, otherwise, it's set to "normal". The heading.text is displayed as the link text.
By following these steps, you can create a functional and interactive table of contents in your React application. The table of contents will automatically update based on the content on the page and highlight the active section as the user scrolls. This navigation feature provides an intuitive way for users to navigate lengthy documents or articles with ease.

## Wrapping Up
In this tutorial, We learned how to make a table of contents with item highlighting to signify each active header, assisting people in navigating your site and enhancing overall UX.