Left Arrow


Table of Contents

A web page wireframe showing a table of contents and sections of content.

Table of Contents

Intended Audience: Front-end developers.


Status: seed

How to create a Table of Contents UI (User Interface) widget.


Common features of a table of contents widget are:

  • a list of links, each relating to a section of content,
  • clicking a link scrolls the page to the related section,
  • when a section is in the viewport, the related link is highlighted and
  • the list remains in the viewport, regardless of how far the user scrolled down.

Initial Render

We start by creating the elements for the initial render. Make the body a flex container and add two children:

  • a ul on the left that will render the links and
  • main on the right that will render each section of the page's content.
An isometric view of the DOM. Showing the viewport, a list of links on the left and a list of sections on the right.


      <a class="highlight" href="#one">1</a>
      <a href="#two">2</a>
      <a href="#three">3</a>
   <section id="one">1</section>
   <section id="two">2</section>
   <section id="three">3</section>


To keep the links in the viewport while the user is scrolling, add position: sticky; top: 0; to the ul. By default, a flex container stretches its children to 100% of its height. This means the body and ul will be the same height. As the body will be the scrolling element, this makes position: sticky irrelevant. To prevent the ul from stretching, set align-self: flex-start.

An isometric view of the DOM. Showing the viewport, a list of links on the left and a list of sections on the right.


ul {
   align-self: flex-start;
   position: sticky;
   top: 0;

   display: flex;
   flex-direction: column;


To highlight a link when its related section scrolls into the viewport, we can use the Intersection Observer. In its options object:

  • omit the root property to set the viewport as the root,
  • set threshold: 0 to trigger the callback when 1px of a target element intersects the root and
  • set rootMargin: "-50% 0px". Making the intersection area a small gap in the middle.
A wireframe of the viewport showing 2 shaded areas at the top and bottom

Observe each section. When 1 is scrolled into the middle of the viewport, the callback will be executed. Use the callback to update which link is highlighted. In the sandbox below, root is set to document. This is only required because it is being rendered in an iframe.


const links = document.querySelectorAll("a");
const sections = document.querySelectorAll("section");
const options = {
   // root is only required because this sandbox is in an iframe.
   root: document,
   rootMargin: "-50% 0px",
   threshold: 0,
const HIGHLIGHT_CLASS = "highlight";
const tableOfConentsMap = [...sections].reduce(
   (acc, section, i) => ({
      [section.id]: links[i],

let selectedId = sections[0].id;

function removeHighlight(id) {

function addHighlight(id) {

function onObserve(entries, observer) {
   entries.forEach((entry) => {
      if (entry.isIntersecting) {
         const { id } = entry.target;
         selectedId = id;

const observer = new IntersectionObserver(onObserve, options);

sections.forEach((section) => {

Where to Next?

Arrow pointing downYOU ARE HERE
Table of Contents
A sci-fi robot taxi driver with no lower body