Add a table of contents (TOC) to your blog
Tags: webdev, beginners, tutorial, javascript
Published: 2022-06-27 10:43:41


A table of contents (TOC) can serve as a summary for your page and enable readers to quickly navigate its contents.

You may be thinking, "boring, this has been loads of time before and is easy". Actually, I have seen nothing clear and concise written about this topic, especially something that is beginner-friendly, and that considers accessibility properly. Often, the examples focus on doing something fancy such as having the table of contents (TOC) display your current position in the document.

So, let's cover the basics really well and give you a spring-board to get fancy.

## Solutions for generating a TOC

Many of the popular [static site generators (SSGs)](https://www.cloudflare.com/learning/performance/static-site-generator/) and [content management systems](https://kinsta.com/knowledgebase/content-management-system/) have this functionality built-in, available as a plugin, or through the markdown processor that they use. What is typically generated for you is an ordered lists (`ol`) of links that point to the headings in the page. ID generation is required to make your headings (or sections) referencible. Usually an unique [slug](https://itnext.io/whats-a-slug-f7e74b6c23e0?gi=7fdc03c50816), a human-readable ID, is generated and added to each heading in their `id` attribute. We will cover the specific HTML in the next section.

You probably have some options to configure the TOC such as:

- You can specify the location of the TOC with a class name or through some specific markup,
- Include only certain heading levels,
- Choose to make it an ordered list (`ol`) or unordered list (`ul`),
- Exclude specific headings.

What most solutions do for you is to add the HTML for the TOC to your webpage. It is up to you to style it, and add functionality.

Here are what the solutions offered by some of the popular SSGs:

1. [Jekyll](https://jekyllrb.com/) has the [jekyll-toc](https://github.com/toshimaru/jekyll-toc) plugin. I can vouch for this one, as I use it on an active website. It has excellent configuration options. It is one of the few I have seen where you can exclude individual headings within a page, so you can slim down the size of the table of contents.
2. The [Kramdown](https://kramdown.gettalong.org/) markdown parser-converter that is used by Jekyll, has the ability to [generate a TOC](https://kramdown.gettalong.org/converter/html.html#toc). If all is want is a TOC, it does the job.
3. [Eleventy](https://www.11ty.dev/) has the [eleventy-plugin-toc](https://www.npmjs.com/package/eleventy-plugin-toc) plugin.
4. [Hugo](https://gohugo.io/) has [built-in support for adding a TOC](https://gohugo.io/content-management/toc/).

If you use a JavaScript application framework such as Gatsby (React) or Next (React) or Nuxt (Vue), you will find that people have probably made a plugin, or posted a solution for adding the functionality. For example, [Gatsby](https://www.gatsbyjs.org/) has the [gatsby-remark-table-of-contents](https://www.gatsbyjs.com/plugins/gatsby-remark-table-of-contents/) plugin.

WordPress has [a lot of options](https://www.contentpowered.com/blog/add-table-contents-wordpress/). For example, there is the [Easy Table of Contents plugin](https://wordpress.org/plugins/easy-table-of-contents/).

## Basic HTML and styles

Let's look at the HTML generated typically and see if we should add anything to it.

### HTML

This is typical of the HTML that is generated for you:

```html
<ol id="toc-list">
  <li>
    <a href="#how-do-i-use-shortcuts">How do I use shortcuts?</a>
  </li>
  <li>
    <a href="#how-to-add-and-modify-keybindings">How to add and modify keybindings</a>
  </li>
  <!-- More list items here, which may contain nested lists -->
</ol>
```

Since we have a group of links that we use to navigate to different sections of the page, it makes sense to wrap them in a [`nav` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav).

As [MDN says](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav):

> The **`<nav>`** HTML element represents a section of a page whose purpose is to provide navigation links, either within the current document or to other documents. Common examples of navigation sections are menus, tables of contents, and indexes.

We wrap our `ol` with a `nav`, and include a `h2` heading, as below:

```html
<nav class="toc" aria-labelledby="secondary-navigation">
  <h2 id="secondary-navigation">Table of Contents</h2>
  <ol id="toc-list">
      <!-- same as before -->
   </ol>
</nav>
```

Since a `nav` is a [landmark](https://www.w3schools.com/accessibility/accessibility_landmarks.php) element, we should give it a label. We should label our `nav` so that it can be identified by [assistive technologies](https://webaccess.berkeley.edu/resources/assistive-technology) such as screen readers.

We can label our `nav` using the [`aria-labelledby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby) attribute that points to the heading. The value of the `id` is provided as the value to `aria-lablledby` to link them. The `h2` should concisely describe the purpose of the section to be used as the label.

If you do not have a heading (you should), or it does not concisely describe the `nav`, you can use the [`aria-label`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label) attribute on the `nav` instead.

### Styles

Our blog posts will have a single column layout. This is the most common layout.

Elsewhere around the web, you may have seen a two column layout with the TOC in a sidebar. This is more common for documentation-centric websites like [MDN web docs](https://developer.mozilla.org/). They tend to hide the TOC for smaller screen sizes.

We won't get into different layouts. We can touch on some options of displaying the TOC differently in the [*Fancy features*](#fancy-features) section.

Our TOC is a group of nested lists. So, you can [style it as a list](https://developer.mozilla.org/en-US/docs/Learn/CSS/Styling_text/Styling_lists) any way you want! You can use list-specific properties for the following:
- You can change the style of the markers (the bullets or numbers) with the [`list-style-type`](https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type) property,
- change the appearance of the markers through the [`::marker`](https://developer.mozilla.org/en-US/docs/Web/CSS/::marker) psuedo-element,
- have images for markers through [`list-style-image`](https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-image) property,
- and even make you own numbering scheme through [CSS counters](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Counter_Styles/Using_CSS_counters).

Based on the default browser styles, our table of contents will look something like this:

![unstyled table of contents](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/unstyled-toc.png)

It is a bit ugly!

The numbering of the nested lists makes it a bit confusing to read. The numbers restart at 1 at every level. It is probably better to remove the numbers and use the indentation to show the levels, or to use an ordinal numbering scheme.

Let's start by giving it a clear appearance as its own section.

#### Basic appearance

First, we will add some styles to make it stand out a bit. We can give the `nav` a darker background colour, a subtle border, and we will round the corners with `border-radius` .

```css
nav {
  background-color: #fffff9;
  border: 1px solid lightgrey;
  border-radius: 5px;
    
  display: flex;
  flex-direction: column;
  align-items: center;
  row-gap: 1rem;  
}
```

Next, we will make it a flexbox container (`display: flex`), mainly so that we can center the items. We change the `flex-direction` to "column" to have the items in a single column. Then, we center everything with `align-items: center;` and we can also control the space between the items with `row-gap`.

![toc basic styles1](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/toc-basic-styles1.png)

Let's style the links. I think removing the underline and using a less saturated colour would be an improvement. You can use the `hsl()` color function and reduce the second number to get a less saturated color.

```css
.toc a {
  text-decoration: none;
  color: hsl(193, 46%, 39%);
}
```

And it looks like this:

![toc basic styles 2](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/toc-basic-styles2.png)

The spacing looks a bit off. By default, the `h2` and `ol` have a `margin-top` and `margin-bottom`. Let's remove these by setting margin to zero, and we will just be relying on `row-gap` to set the space between them. However, now the heading and list right on the edges of the `nav`, so let's add `padding` to the table of contents itself to let them breathe a bit!

```css
.toc h2,
.toc ol {
  margin: 0;
}

.toc nav{
   /* same styles as before */
    
   padding: 1rem;
}
```

![toc spacing adjusted](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/toc-spacing-adjusted.png)

It looks more organised, right?

Good, but we're not done!

The next thing we will probably want to change is the numbered markers. Let's explore 2 options for this. First, we will look at removing the markers and use indentation to show the hierachy. Then, we will look at using an ordinal numbering scheme for the markers.

#### Remove markers and indent

To remove the markers is simple. You just use `list-style-type:none;` on the `ol` element.

Now, let's consider the padding. By default, an `ol` has a `padding-right`, the value is 40 pixels in Firefox. We can turn on the flex outline in the Firefox's devtool to see the spaces. I check the box as circled in green in the screenshot below to show the outline.

![toc flex outline](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/toc-flex-outline.jpg)

What I would like is for the top `ol` to have no padding, and any nested `ol` to have `padding-right` of 2 rem. We can add a  `padding-right`  of 2 rem to all `ol` with the selector `.toc ol`. And subsequently we can target the top `ol` to give it a padding of zero by using the  `>` child combinator, the selector would be `.toc > ol`.

I think it is a good habit to use [logical properties](https://css-tricks.com/css-logical-properties-and-values/) because the [browser support is very good now](https://caniuse.com/css-logical-props). Logical properties allow declaring styles that work with all writing systems, so I will use `paddding-inline-start` instead of `padding-right`! This means the CSS below should give the desired outcome for a right-to-left language such as Arabic, as well as a left-to-right language such as English

```css
.toc ol {
  list-style-type: none;
  padding-inline-start: 2rem;
}

.toc > ol {
  padding: 0;
}
```

Here is the result:

{% jsfiddle https://jsfiddle.net/robjoeol/wsjkun45/ result,html,css %}

I think it is a good improvement from what we had originally! You may still want to tweak it, or take it in another direction!

#### Ordinal numbering

Maybe the most common style that I have seen for a table of contents is an ordinal numbered list. What do I mean by ordinal numbering?

Each item is numbered according to the order of its heading level, beginning with the first `h2` heading. This is usually in the format of `<h2-sequence-number>.<h3-sequence-number>.<h4-sequence-number>.` . It is probably easier to understand by exploring a specific example!

Below is an example of table of contents from Wikipedia, [an article about the River Lee](https://en.wikipedia.org/wiki/River_Lee):

![wikipedia-example-toc](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/wikipedia-example-toc.png)

You can see how the numbers corresponds to the headings. The link to the first `h2` is given the number *1*. The link to the second `h2` is given the number *2*, with it's first `h3` given the number *2.1*, and so on.

But remember, the default styling for nested ordered lists restarts the numbering for each list (as below)!

![ol-default-styling](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/ol-default-styling.png)

We can use [CSS counters](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Counter_Styles/Using_CSS_counters) to number the items the way we would like.

To use a counter, it must first be initialized with the [`counter-reset`](https://developer.mozilla.org/en-US/docs/Web/CSS/counter-reset) property.  Below we initialize a counter and name it `toc-counter` . By default, counters have an initial value of 0, and count up from the initial value.

```css
.toc ol {
    list-style-type: none;
    
    /* initialise counter */
    counter-reset: toc-counter;
}
```

We remove the existing numbers for our list by setting `list-style-type: none;`. We are going to put our custom numbers into a `::before` pseudo-element instead.

Once initialized, a counter's value can be incremented using [`counter-increment`](https://developer.mozilla.org/en-US/docs/Web/CSS/counter-increment).  We increment the counter in a `::before` pseudo-element for the list items. The value of the counter can be displayed using the [`counters()`](https://developer.mozilla.org/en-US/docs/Web/CSS/counters) function in the [`content`](https://developer.mozilla.org/en-US/docs/Web/CSS/content) property of the pseudo-element, as below.

```css
.toc li::before {
  /* display the counter formatted as our ordinal style */
  content: counters(toc-counter, ".") " "; 

  counter-increment: toc-counter;
   
  color: hsl(14, 51%, 54%); /* reddish brown color */
}
```

The [`counters()`](https://developer.mozilla.org/en-US/docs/Web/CSS/counters) function has two forms: `counters(<counter-name>, <separator>)` and `counters(<counter-name>, <separator>, <counter-style>)`.  We will use the latter form. Our *separator* will be a dot (period). For the *counter-style*, we provide a space. This adds a space to the end of the counter text that will separate the number from the text content of the list item.

And this is the result:

{% jsfiddle https://jsfiddle.net/robjoeol/q2n1c0zt/ result,html,css %}

#### Smooth scrolling to sections

When clicking internal links, it can be a more pleasant experience to smoothly scroll down to the section rather instantly jumping to it. To achieve this, you can add this to your stylesheet:

```css
html {
	scroll-behavior: smooth;
}
```

## Collapsible functionality

It would be nice to be able to show the TOC in a minimized or maximized state. If it is a bigger TOC, we may prefer to have the content hidden initially, and let it up to the user to show it.

This functionality is an example of the [disclosure UI pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#disclosure). There are two main approaches to implementing this, and neither is perfect. It is one of those areas that has some shortcomings and should be easier by now!

### Option 1) Use `details` and `summary`

We can use the combination of the [`details`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) and [`summary`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary) elements to provide this functionality. These two together are usually referred to as a [“disclosure widget”](https://html.spec.whatwg.org/multipage/interactive-elements.html#the-details-element).

{% jsfiddle https://jsfiddle.net/robjoeol/hty19jva/ result,html,css %}

<h4 class="no_toc">HTML</h4>

```html
<div class="toc">
	<details open>
		<summary>
			<h2>Table of Contents</h2>
		</summary>
		<nav aria-label="table of contents">
			<ol id="toc-list">
				<!--items here-->
			</ol>
		</nav>
	</details>
</div>
```

We need to wrap everything in a `div`, so that we can style it later. You cannot layout `details` as a flexbox or grid container as you would expect. The `summary` element is treated like an implicit element, so it is not laid out as a flexbox/grid item.

By default, the disclosure widget is closed. We add the `open` attribute to show the contents.

I added an `aria-label` to the `nav` to give it an accessible label, however I am not sure if it is possible to make this example fully accessible. The issue is that `summary` has an implicit ARIA role of [`button`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role), this means that it eats the semantics of elements inside it. So our `h2` contained in the `summary` is treated like a `span` really! In cases where the TOC is in a closed state, it will not be announced by screen readers.

If you know of a clear way to make this fully accessible, let me know!

<h4 class="no_toc">CSS</h4>

The default appearance of the `summary` is:

- When the content is hidden, it is styled with a right-pointing triangle to hint that activating the button will display additional content.
- When the content is visible, the triangle points down.

Left unstyled, disclosure widgets present us with two issues:

1. The `summary` cursor icon: Though the `summary` section is an interactive element, the element’s default cursor is a text selection icon rather than the pointing hand that you may expect. The pointing hand hints to a user that clicking is possible. This is desirable!
1. Block elements in `summary` are positioned on the line below the marker, like in the screenshot below.

   ![default-gap](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/default-gap.png)

To rectify these issues, we can add the following styles to "reset" the behavior to what we would typically expect:

```css
summary { 
  cursor: pointer;
  user-select: none;
}

summary > * {
  display: inline;
}
```

To center everything, we wrap our `nav` in a `div`, and make it a flexbox container. We still need to use `text-align:center;` on the `h2` to center it as it is a rogue element!

To create space between the `h2` and the `nav`, we add a `margin-block-start` to the `nav`. We won't use `row-gap` because of the `summary` issue.

```css
.toc {
  display: flex;
  flex-direction: column;
  align-items: center;
}

nav {
  margin-block-start: 1rem;
}

h2{
	text-align: center;
}
```

<h4 class="no_toc">Pros</h4>

This solution has **no JavaScript**.

It **supports keyboard interaction** when it has focus. <kbd>Enter</kbd> or <kbd>Space</kbd> activates the disclosure control and toggles the visibility of the disclosure content.

<h4 class="no_toc">Cons</h4>

**Accessibility is a mixed bag**. The `summary` element is like a button and buttons do not respect the semantics of child elements. Some screenreaders will treat everything inside the `summary` element like a `span`. So our `h2` will not be announced properly to a user with a screen reader. Dave Rupert discusses this further in his article, [Why `details` is Not an Accordion](https://daverupert.com/2019/12/why-details-is-not-an-accordion/). You can see the [latest accessibility testing results](https://a11ysupport.io/tests/tech__html__details-summary) to see the current state on this.

**Styling is a bit awkward**. Not being able to make `details` a flexbox or grid container is limiting.

Styling of the marker is a bit limited too. Surprisingly, it is done through list-style properties, you can provide an image to `list-style-image`. When you specify your own marker, you cannot add any transitions when it changes from an open to closed state.  

Overall, the implementation of `details` and `summary` is a bit ragged really.

### Option 2) Use a `button` and JavaScript for interactivity

{% jsfiddle https://jsfiddle.net/robjoeol/6da74g2y/ result,html,css,js %}

<h4 class="no_toc">HTML</h4>

```html
<nav class="toc" aria-labelledby="toc-heading">
	<header>
		<h2 id="toc-heading">Table of Contents</h2>
		<button aria-controls="toc-list" aria-expanded="true">Hide</button>
	</header>
	<ol id="toc-list">
		<!-- items here -->
	</ol>
</nav>
```

We add the following aria attributes to make it fully accessible:

- [`aria-labelledby="IDREF"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby): Added to the `nav` to give it an accessible name. We link it to the `h2`.
- [`aria-expanded`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded): Indicates that the container controlled by the disclosure button is *hidden* when the value is `false` and visible when the value is `true` . Set the value to `true` as we show the contents by default.
- [`aria-controls="IDREF"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls): The disclosure button controls visibility of the container identified by the `IDREF` value. Alternatively, you could use [aria-owns](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-owns).

<h4 class="no_toc">CSS</h4>

There are [many ways to hide elements in CSS](https://www.sitepoint.com/hide-elements-in-css/).

We want the following:
1. We want the space to collapse when the content is hidden (no empty space left behind),
1. The content to be hidden from the accessibility API when it is a hidden state. I am not totally sure if this is necessary if we use `aria-expanded`.

If you also want to animate the transition as well, it is trickier to find a solution that checks all 3 boxes.

Without animation, looking at our options:
- [`visibility:collapse`](https://developer.mozilla.org/en-US/docs/Web/CSS/visibility) has issues, according to MDN: "Support for visibility: collapse is missing or partially incorrect in some modern browsers."
- The properties `transform`, `color`, `opacity`, `visibility: hidden` and `clip-path` all leave empty space when we are in the "hidden" state. Overlaying a pseudo-element has this issue also.
- Reducing dimensions such as `height` means the content is always accessible. We could toggle the value of `aria-hidden` ourselves. It triggers rerendering the page layout, which we prefer to avoid if possible.
- `display: none` is the most commonly used. Resetting to the previous style is usually fine, but keep in mind that it has many options such as `block` and `inline`. It triggers rerendering the page layout, which we prefer to avoid if possible.
- Absolute positioning to move it offscreen, but you would probably need to combine it with `opacity` for it to work effectively.

There is no clear winner! We will go with `display: none` to keep things simple (sigh).

```css
.hide{
    display:none;
}
```

We can toggle this class in JavaScript.

<h4 class="no_toc">JavaScript</h4>

```javascript
const button = document.querySelector(".toc button");
const content = document.querySelector("#toc-list");

button.addEventListener("click", toggleTableOfContents);

function toggleTableOfContents() {
  content.classList.toggle("hide");

  if (button.innerText === "Hide") {
    button.innerText = "Show";
    button.setAttribute("aria-expanded", false);
  } else {
    button.innerText = "Hide";
    button.setAttribute("aria-expanded", true);
  }
}
```

The button supports keyboard interaction when it has focus by default, so we do not need to do add any keybindings. We change the state of `aria-expanded`, so that its state is available to the assistive technologies.

<h4 class="no_toc">Pros</h4>

This solution is **completely accessible** (correct me if I missed something).

We have **complete control over the style of the disclosure button**.

<h4 class="no_toc">Cons</h4>

**JavaScript is required**.

### Option 3) Use a link (`a` element) and JavaScript for interactivity

Don't do it! Go with option 2 instead of this option!

Why?

A good rule of thumb is: buttons are for actions ("do something for me"), whereas links are for navigation ("take me to a different location)". Chris Ferdinandi spoke about this recently in his [HTML semantics daily tip](https://gomakethings.com/html-semantics/#buttons-and-links-do-different-things).  Not everyone is taught this, so I am glad that this message is being put out there more often.

You may be thinking but doesn't Wikipedia use a link for this behaviour?

Yes, they do. And do not copy them! 😁

### Which option should I chose?

Option 1 is the simplest and being JavaScript-free is always great. However, if you care about maximizing the accessibility of your website (you should), then option 2 is the better option for now. I am not accessibility expert, but I don't see a way to get it right at the moment with option 1.

## Fancy features

Here I will skim through some examples of fancy things you can do. I will only touch on the code, but I will point to a tutorial if there is a good one out there. I may tackle one of the fancy examples in another post, you can make a request if you wish!

### Sticky single column layout

It can be helpful to the reader to have the TOC always in view.

The simplest way to do this is to make add `position: sticky;` to the TOC. You probably want it to stick to the top of the window, so you can it use along with the `top` property.

```css
.toc {
	position: sticky;
	top: 0;
}
```

One thing to keep in mind is that if your TOC is inside a grid or flex container, you may want to add `self-align:start` to it to ensure it is not out of reach to become sticky! You need to be able to scroll beyond an element for it to switch to a fixed position, so sometimes if you align or justify an element, it can hamper this from happening.

{% jsfiddle https://jsfiddle.net/robjoeol/xm3d86y7/ result,html,css,js %}

The obvious drawback of this approach is that you don't want the TOC to hog the screen when the user wants to read the article. Here we have it in a minimized state initially and leave it up to the user to view if they want to.

I think that this is alright, but it could be taken further to make it a better user experience. It can be a bit confusing when you want to scroll the TOC to see the other items, and actually the page scrolls in the background instead. It depends on where the focus is! An overlay that freezes the scrolling of the page would make it easier to use.

An alternative is to have the TOC shrink to a button, once it becomes sticky. [Pawel Cislo](https://pawelcislo.com/) does something like this. The TOC is maximised inline in the article until you scroll down a certain amount, and then it appears minimized as a button in the right of the article. You can see in the video below, or [visit this blog post](https://pawelcislo.com/2021/11/14/my-vs-code-playground/) to see in action yourself.

<video preload="none" poster="https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/pawel-cislo-toc.jpg" controls="" muted="">
	<source src="https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/pawel-cislo-toc.mp4" type="video/mp4">
</video>

I think it would benefit from a transition effect of the TOC to a button to make it clearer to the user what happened. There is a big distance between the original position and new position.

[Hashnode](https://hashnode.com/) has a nice variation of this. Hashnode is a blogging platform if you are unfamiliar with it. By default, the TOC is maximised. When you scroll beyond the TOC, the TOC is minimized and becomes sticky tab pinned to the top of the window. You can see it in action in the video below, or [visit this blog post](https://eedris.hashnode.dev/a-comprehensive-guide-to-creating-and-publishing-your-first-npm-package) to try out yourself.

<video preload="none" poster="https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/hashnode.png" controls="" muted="">
	<source src="https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/hashnode-toc.mp4" type="video/mp4">
</video>

It quite an elegant solution for a single column layout. Although, I think the transition is a bit abrupt.

Ben Holmes takes a different tack on [his blog](https://bholmes.dev/blog/). A button pops up as you scroll through the post. Underneath the button, it shows the name of the section you are in. Pressing the button will open the TOC with the current section highlighted. You can see it in action in the video below.

<video preload="none" poster="https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/ben-holmes-toc-fullsize.png" controls="" muted="">
	<source src="https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/ben-holmes-toc.mp4" type="video/mp4">
</video>

It works quite well. In particular, he has made smart use of space on smaller screens by putting the button and section heading into a sticky nav bar.

![ben holmes blog on mobile with sticky nav table of contents](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/ben-holmes-toc-mobile.png)

The only criticism I have is that you can't scroll through the TOC really. The actual page scrolls instead. An overlay might work here to enable targeted scrolling.

### Sticky two column layout

It is quite common to have a sticky TOC in a sidebar, like the [MDN web docs,](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#browser_compatibility) as in the screenshot below.

![mdn-sidebar-sticky-toc](https://www.roboleary.net/assets/img/blog/2022-06-27-add-a-table-of-contents-to your-blog-posts/mdn-sidebar-sticky-toc.png)

MDN hides the TOC for smaller screen sizes when it switches to a single column layout. You can use a [media query](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Media_queries) to do this.

I would like to see to see a switch to something similar to our single column layout for smaller screen sizes if it can be executed well.

### Show your progress in the page (scrollspy)

Another fancy thing people like to add is a progress scrollspy. As you go through the article, the section you are in will be highlighted in the TOC. This is usually implemented in a 2-column layout with the TOC in the sidebar.

You can see this in the wild in the following places:
- [Ben Holmes](https://bholmes.dev/blog/): As shown previously, Ben has this on his blog,
- [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API): The TOC is only visible on screens greather than 800px,
- [Josh Comeau's blog](https://www.joshwcomeau.com/): The TOC is only visible on screens greater than 1100px.

Bramus Van Damme has [a nice tutorial](https://www.bram.us/2020/01/10/smooth-scrolling-sticky-scrollspy-navigation/) on how to make this yourself. He covers semantic markup, how to implement most of the functionality with HTML and CSS, and then how to do the last bit with JavaScript.

Below is the complete example from that tutorial:

{% codepen https://codepen.io/bramus/pen/ExaEqMJ %}

There are many variations on this too. It is a popular bit of blink people like to implement.

I don't know if it is all that useful though!

## Wrapping up

We covered a lot of ground! I hope I was able to break down the different considerations of adding a TOC to your blog.

From the outset, it looks simple. However, to make it look nice, and make it accessible to as many people as possible, it requires more effort. And of course, there are plenty of ways you can dress it up! 🎩🧐