Create a table of contents (TOC) in Webflow in 5 minutes

Create a table of contents (TOC) in Webflow in 5 minutes

June 4, 2023
Reading time:
5 min
Table of content
Table of Contents (ToC) is a useful feature for organizing and navigating through long-form content on a website. It allows users to quickly jump to specific sections, improving user experience and content accessibility. I would like to guide you through the process of creating a Table of Contents on Webflow using a custom script.

Let's create a script that automates the process of generating a Table of Contents for your Webflow website. This script will dynamically create a list of links based on the headings in your content and add scroll-to functionality. It works with CMS and static content. Now, let's delve into the details and understand how this script works. If you need a copy-paste solution, please open the clonable Webflow project.

Prepare Your Webflow Project

Make sure you have a Webflow project set up with the necessary content. Identify the container where you want to display the Table of Contents. In our example, we use the <div id="toc"> element.Make sure to structure your content using the appropriate heading tags (h2, h3, h4). These headings will be utilized to generate the Table of Contents. To create a Table of Contents, ensure that your content is contained within an element with the ID <div id="single-article">. If you choose to change the ID name, remember to update the corresponding CSS and JavaScript accordingly.

Event Listener and Element Retrieval

We begin by adding an event listener to the DOMContentLoaded event. This ensures that the code is executed when the HTML document has finished loading and the DOM is ready for manipulation. Inside this event listener, we retrieve references to the article and tocContainer elements using their respective IDs: "single-article" and "toc".


// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {

  // Get the article container and the TOC container
  const article = document.getElementById("single-article");
  const tocContainer = document.getElementById("toc");

TOC function

Next, we define the createTOC function, which is responsible for generating the Table of Contents (TOC) based on the headings found within the article element. This function selects all the h2, h3, and h4 headings within the article and creates a TOC item for each heading. The TOC item is a list item (li) containing an anchor link (a) with the heading title as its text content. The anchor link is assigned an href attribute that points to the corresponding heading's ID. The generated TOC items are appended to a document fragment for efficiency and then inserted into the tocContainer element.


  // Create the Table of Contents (TOC)
  const createTOC = () => {
    // Get all the headings inside the article
    const headings = article.querySelectorAll("h2, h3, h4");
    const tocFragment = document.createDocumentFragment();

    // Loop through each heading
    headings.forEach((heading) => {
      // Get the title and create an anchor ID
      const title = heading.textContent.trim();
      const anchorId = `toc-${title.toLowerCase().replace(/\s+/g, '-')}`;

      // Set the anchor ID as the heading's ID
      heading.id = anchorId;

      // Create a list item with an anchor link
      const li = document.createElement("li");
      const anchor = document.createElement("a");
      anchor.textContent = title;
      anchor.href = `#${anchorId}`;
      li.appendChild(anchor);
      tocFragment.appendChild(li);
    });

    // Create a nested unordered list and append the TOC items
    const ul = document.createElement("ul");
    ul.appendChild(tocFragment);
    tocContainer.appendChild(ul);
  };
  

Generating the TOC

After defining the createTOC function, we check if both the tocContainer and article elements exist. If they do, we call the createTOC function to generate the TOC.


  // Check if the TOC container exists and the article has headings
  if (tocContainer && article) {
    createTOC(); // Call the function to generate the TOC
  }
  

Handling Click Events

Next, we select all the TOC items and section titles within the content area. These elements will be used to handle click events and highlight the active TOC item.

Setting the Active TOC Item

We define the setActiveItem function, which is responsible for setting the active item in the TOC. This function iterates over all the TOC items and adds the 'active' class to the item whose href attribute matches the target ID. It removes the 'active' class from the rest of the items.


 // Add click event listeners to TOC items
  tocItems.forEach(function(item) {
    item.addEventListener('click', function(event) {
      event.preventDefault();
      var targetId = this.getAttribute('href').substring(1);
      setActiveItem(targetId);
      document.getElementById(targetId).scrollIntoView();
    });
  });

  // Add click event listeners to section titles
  titleElements.forEach(function(title) {
    title.addEventListener('click', function() {
      var targetId = this.getAttribute('id');
      setActiveItem(targetId);
    });
  });

Click Event Listeners

We attach click event listeners to all the TOC items. When a TOC item is clicked, the listener prevents the default link behavior, retrieves the target ID from the clicked item's href attribute, sets the active item using the setActiveItem function, and scrolls to the corresponding section by finding the element with the matching ID and calling scrollIntoView().

Similarly, we attach click event listeners to all the section titles. When a section title is clicked, the listener retrieves the target ID and sets the active item using the setActiveItem function.

Tracking Intersection with Viewport

We create an IntersectionObserver to track the intersection of the headings with the viewport. When a heading intersects with the viewport, we remove the 'active' class from other TOC items and add the 'active' class to the corresponding TOC item using the heading's ID.

Observing Headings for Intersection

If there are headings present (checked by the condition "h2,h3,h4" !== ""), we select all the headings within the article element and observe them for intersection using the IntersectionObserver.


  // Intersection Observer for highlighting active TOC item
  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      const id = entry.target.getAttribute("id");
      if (entry.isIntersecting) {
        // Remove the 'active' class from other TOC items
        document.querySelectorAll(".active").forEach((z) => {
          z.classList.remove("active");
        });
        // Add the 'active' class to the corresponding TOC item
        document.querySelector(`a[href="#${id}"]`).classList.add("active");
      }
    });
  }, { rootMargin: '0px 0px -50% 0px' });

  // Observe the headings for intersection
  if ("h2,h3,h4" !== "") {
    document.getElementById("single-article").querySelectorAll("h2, h3, h4").forEach(function(heading) {
      observer.observe(heading);
    });
  }

Optional: Handling Anchor Position

We can define the offsetAnchor function to handle the anchor position when the page loads or the hash changes. If the hash in the URL is not empty, the function retrieves the target ID from the hash, calculates the offset to scroll to the target element, considering any fixed headers, and scrolls to the target element.

We add event listeners for hash changes and call the offsetAnchor function to handle the initial anchor position when the page loads.


  // Handle anchor position when the page loads or the hash changes
  function offsetAnchor() {
    if (location.hash.length !== 0) {
      // Get the target ID from the hash
      const targetId = location.hash.substring(1);
      const targetElement = document.getElementById(targetId);
      if (targetElement) {
        // Calculate the offset to scroll to the target element, considering any fixed headers
        const offset = targetElement.getBoundingClientRect().top - 100;
        window.scrollTo(window.scrollX, window.scrollY + offset);
      }
    }
  }

  // Add event listeners for hash changes and call the offsetAnchor function
  window.addEventListener("hashchange", offsetAnchor);
  window.setTimeout(offsetAnchor, 1);
});

Final code


 document.addEventListener('DOMContentLoaded', function() {
  const article = document.getElementById("single-article");
  const tocContainer = document.getElementById("toc");

  // Create the TOC
  const createTOC = () => {
    const headings = article.querySelectorAll("h2, h3, h4");
    const tocFragment = document.createDocumentFragment();

    headings.forEach((heading) => {
      const title = heading.textContent.trim();
      const anchorId = `toc-${title.toLowerCase().replace(/\s+/g, '-')}`;

      heading.id = anchorId;

      const li = document.createElement("li");
      const anchor = document.createElement("a");
      anchor.textContent = title;
      anchor.href = `#${anchorId}`;
      li.appendChild(anchor);
      tocFragment.appendChild(li);
    });

    const ul = document.createElement("ul");
    ul.appendChild(tocFragment);
    tocContainer.appendChild(ul);
  };

  // Check if the TOC container exists and the article has headings
  if (tocContainer && article) {
    createTOC();
  }

  var tocItems = document.querySelectorAll('#toc a');
  var titleElements = document.querySelectorAll('.content [id]');

  function setActiveItem(targetId) {
    tocItems.forEach(function(item) {
      if (item.getAttribute('href') === '#' + targetId) {
        item.classList.add('active');
      } else {
        item.classList.remove('active');
      }
    });
  }

  tocItems.forEach(function(item) {
    item.addEventListener('click', function(event) {
      event.preventDefault();
      var targetId = this.getAttribute('href').substring(1);
      setActiveItem(targetId);
      document.getElementById(targetId).scrollIntoView();
    });
  });

  titleElements.forEach(function(title) {
    title.addEventListener('click', function() {
      var targetId = this.getAttribute('id');
      setActiveItem(targetId);
    });
  });

  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      const id = entry.target.getAttribute("id");
      if (entry.isIntersecting) {
        document.querySelectorAll(".active").forEach((z) => {
          z.classList.remove("active");
        });
        document.querySelector(`a[href="#${id}"]`).classList.add("active");
      }
    });
  }, { rootMargin: '0px 0px -50% 0px' });

  if ("h2,h3,h4" !== "") {
    document.getElementById("single-article").querySelectorAll("h2, h3, h4").forEach(function(heading) {
      observer.observe(heading);
    });
  }

  // handle anchor position
function offsetAnchor() {
  if (location.hash.length !== 0) {
    const targetId = location.hash.substring(1);
    const targetElement = document.getElementById(targetId);
    if (targetElement) {
      const offset = targetElement.getBoundingClientRect().top - 100;
      window.scrollTo(window.scrollX, window.scrollY + offset);
    }
  }
}

window.addEventListener("hashchange", offsetAnchor);
window.setTimeout(offsetAnchor, 1);
});

That's it! With the above code implemented on your Webflow website, you'll have a functional and dynamic Table of Contents. Feel free to customize the styling and appearance of the TOC to match your website's design.