Back to gallery

Design engineering: a table of contents component

This component (shown on the left hand-side, play with it!) was crafted at beam, a browser with a first-class note taking experience. With beam, you can point and shoot elements from the pages you’re browsing and build rich-media notes out of them, complete with references and back-linking.

The problem (requirements)

Click requirements to expand them

Functional requirements

The table of contents should display a structured list of headings
  • Given a specified html root element
  • In the case of no headings, the component shouldn’t display
Each element in the list should be clickable
  • Clicking an element should scroll to the targeted element
The table of contents should support elements with and without id
  • Scrolling to the targeted element, no matter if it has an id or not
The table of contents should be responsive and adapt to the viewport
  • If the viewport is too small to display the summary links, keep only the collapsed state
The table of contents should update automatically
  • When the html root element changes,
  • or when its content updates
The table of contents component should be fault-tolerant
  • correctly render even in the case where headings are not properly ordered in the document: h1 > h2 > h3 should render the same as h1 > h3 > h5
The table of contents should be scrollable
  • if taller than the viewport, the table of contents should be scrollable

Non-functional requirements

  • The table of contents should be accessible to screen readers
  • The table of contents should be accessible to keyboard users
  • The table of contents should be accessible to mouse users
  • The table of contents should be accessible to touch users
  • The table of contents should be responsive and adapt to the viewport

The solution

The table of contents component was built using React, TypeScript, and (s)CSS. It was designed to be flexible and adaptable to different scenarios. Let’s review some of the requirements and see how we went about.

Anatomy of the Table of Contents component

  • TableOfContents.Root: the table of contents itself, it’s a simple component that calls a private TocElementcomponent to render each entry in the table of content
  • TableOfContents.Provider: a context provider that makes the table of contents available to all its children
  • TableOfContents.useToc: a hook that returns the table of content context, containing the rootElement and a setter for the rootElement

Display a structured list of headings

The table of contents has two states: idle and active. When the table of content is idle, it displays dashes to represent the headings. The dashes length depends on the level of heading it represents. When the table of contents is active, it displays the heading text (truncated if necessary).

Table of contents entries should be clickable and scroll to the targeted element and should support elements with and without ids

Provided the headings have an id, we get this behavior for free by simply linking to the id of the heading using a hash. Should the headings not have an id, we can still provide a clickable element that scrolls to the targeted element, usingelement.scrollIntoView(). In this cas we also apply a temporary class to the targeted element so we can trigger a CSS animation highlighting the targeted element. We use this logic to also enable clicking the dashes when the viewport is too small to display the table of content.

Update automatically

For the automatic updates, we have to take care of two possible events

  • The rootElement itself changes: in the component that renders rootElement, we call th context setter to update it, in case of re-renders that would change the element itself
  • The content of the rootElementrootElement changes: to account for modifications of the subtree of the root element, we use a MutationObserver that triggers a re-render of the table of content

Up next

Details / disclosure component
-->