Inside · Table of contents

Inside · Table of contents

4 min read#Miscellaneous#Behindthescenes

This is part of an ongoing series about how this site is made.

Most posts on this site feature a table of contents to help you understand where you are and where you can go.

I first published a live version of the component nearly four years ago. Since then, I have been making minor tweaks, including a visual update coinciding with the launch of 2.0.

Up until recently, the table of content component was available only on larger screen sizes. I just launched an updated version that brings improvements such as support for smaller screen sizes.


I first remember seeing a beautifully implemented sticky table of contents on the Framer website almost ten years ago. This was before position: sticky; became widely supported, so it’s even more impressive.


I occasionally noticed other examples across the web. For example, I love the fluid zooming animation on the Make Something Wonderful web experience.

make something wonderful

I’ve taken bits and pieces from the various implementations I’ve seen and combined them with the design language I have developed here.

Design details

The basics

To generate the list of items in the table of contents, I get the headings as part of the GraphQL query used to generate each post in Gatsby. I then prepend a “Top” item at the beginning to correspond to the top of the page.

Additionally, I built in some configurability for each post. Some posts are very short, so I turn off the table of contents all together.

In others, the list can get long when there are two or three layers of headings. To solve this issue, I can also set a maximum depth of headings.

Clicking an item in the table of contents will scroll the page to that heading using window.scrollTo().


The most important aspect of the table of contents component is stickiness. This means that it remains on screen as the you scroll down. This is achieved trivially by a combination of position: sticky; and position: absolute;. In larger screen sizes, the table of contents sits to the right of the text.

layout wide

In medium screen sizes, the table of contents transforms into an expandable component that occupies the top of the screen with some space remaining around it. I added a shadow to accentuate that it sits above the other content.

layout mid

In smaller sizes, the table of contents docks to the top and stretches to fit the full screen width.

layout narrow


To make the component work on smaller screen sizes, I made it collapsable.

Initially, I let the heading break across multiple lines. To simplify the layout, I use text-overflow: ellipsis; which limits text to one line by truncating the text and displaying ellipsis.

Transitioning to auto dimensions is difficult. So, this constraint allowed me to use css transitions for animation. I can easily calculate the height of the component in the expanded state based on the number of items.

Active indicator

In large screens, I use a dot to reflect the active heading. This dot follows the color theme of the post.

On smaller screens, when the table of contents is collapsed, I show just the current heading. When expanded, I show the dot.

mobile indicator

To determine which item to highlight as active, I tried a few different algorithms that recalculate the active heading as you scroll. For example, one calculated which section of text takes up the most space on viewable part of the page.

I ultimately chose to look at which heading is closest to the top of the screen. This way, when you click on an item in the table of contents, that item will be reliably set active when the page is scrolled to that position.

Future work

When I started this latest iteration of the table of contents component, I set myself a time budget to work within, à la Shape Up. I knew a long list of things I wanted to improve, but I didn’t want to get bogged down and potentially never ship.

One major thing I cut was making the table of contents accessible. Accessibility is still a weak-point in my skill set, so I knew it would eat up all the time I had allotted for this project. Therefore, in the next future iteration, accessibility will be the focus.

Futhermore, I plan to improve the location that is scrolled to when you click an item in the table of contents. Due to differences in browser layout engines, the behavior is inconsistent.

Finally, I plan to improve the algorithm that determines the currently active item, as there are still some edge cases not handled intuitively.

If you know other ways I could improve this component, please let me know.

Thanks to Q for reading drafts of this. Thanks to Manu for catching a bug. Thanks to Chris for suggesting a usability improvement.

Subscribe to the newsletter