Gatsby Starter Blog MDX

How To Create A Scroll Tracking Table of Content for Gatsby

May 06, 2015

Have you see some documentation and tutorial sites, allow you see which section are you reading on the side? If you want to improve the readability of your Gatsby website. In this article, I will share my process in creating an interactive table of content component that can respond to your scrolling.

Let’s take a look at the requirement:

  1. Reader will be able to navigate section of content easily
  2. Reader will be able to know their reading progress

We will be using a package called react-scrollspy to detect if the headings are in viewport, which will trigger an active class name in return.

By including a Table of Content component, users will be able to navigate within your article easily, finding the content they are interested. Let’s get started and skip to step 2 if you have a Gatsby site already.

1. Create Site

We will install a simple MDX starter at the beginning

gatsby new gatsby-tutorial-table-of-content https://github.com/hagnerd/gatsby-starter-blog-mdx

Use yarn or npm install to initialise the repository.

Taking a look at react-scrollspy, we will need to provide the headings for each page to the component, which luckily generated automatically by default in each post node. We will also need to state the class name to activate when in view.

yarn add react-scrollspy gatsby-remark-autolink-headers

In gatsby-config.js, you will need to add gatsby-remark-autolink-headers within the gatsbyRemarkPlugins.

![[Screenshot 2020-12-11 at 1.57.50 AM.png]]

3. Update Post GraphQL Query

![[20201211-toc-tutorial-graphql-query.png]]

Include tableOfContents in your blog query, in my case it’s blog-post.js in templates folder. After that, if you use GraphiQL, you should be able to see table of contents return as an object. We will call all the url value in the object and return as an array.

![[20201211-toc-tutorial-graphiql-check.png]]

4. Passing Post Props to Toc component

Create a new file called Toc.js in Components folder. We will be adding function step by step to isolate problems that may arise. By adding a very simple component, we need to make sure all the props will be passed through first.

import React from 'react'
export default function Toc(props) {
const { post } = props
console.log(post)
return <h3>Table of Content</h3>
}

Save file and back to blog-post.jsand import Toc component.

import Toc from '../components/toc'

Then I will add Toc just before the MDXRenderer.

<Toc post={post.tableOfContents} />
<MDXRenderer>{post.body}</MDXRenderer>

For now, only the Table of Content h3 title will be displayed. But in Console, as we logged the post props. We can also check if the objects of tableOfContents are returning. ![[Screenshot 2020-12-11 at 1.49.53 AM.png]]

5. Mapping Table of Content Props

For simpler demo we will be only listing all first level heading in the Table of Content. Let’s update the toc.js by mapping the props value. By doing so, we would be able to access all the headings information and link from tableofContentsfrom GraphQL.

return (
<nav>
{post.items.map(p => (
<li key={p.url}>
<a href={p.url}>{p.title}</a>
</li>
))}
</nav>
)

![[Screenshot 2020-12-11 at 8.59.15 AM.png]]

Since we have installed gatsby-remark-autolink-headersto add anchor automatically for each heading. Clicking on the Table of Content would redirect to the heading immediately.

![[Screenshot 2020-12-11 at 2.01.39 AM.png]]

6. Styling Table of Content Component on the side

Now basic functionality has been completed, let’s take care of the styling first show we can test the scrolling easier. Here I will be using the simplest implementation using CSS. But any style implementation should work just fine.

Let’s create a Toc.cssin the same folder and import it in the component.

import './Toc.css'

We will make the Table of Content fixed on the right side of the window, so users can navigate easily no matter which part of the page they are. Also, since we do not want to alter normal list style. We will be specifying class name .toc-list, which will be added in the next step.

We will be also adding a is-currentclass so Scrollspy would able to add the class name when the heading is in view.

nav {
position: fixed;
top: 50vh;
right: 5vw;
margin-left: 36px;
max-width: 250px;
}
ul.toc-list {
border-left: 1px solid #363636;
}
ul.toc-list > li {
list-style-type: none;
margin-left: 24px;
font-size: 13px;
}
ul.toc-list > li > a {
color: #c2c2c2;
text-decoration: none;
border-bottom: 0;
transition: 1s all ease-in-out;
}
ul.toc-list > li.is-current > a {
color: black;
}

7. Implement React Scrollspy

Last but not least, we will need to detect if the heading is in view. react-scrollspywill help us to identify if the anchor is in view after we pass through 2 things.

  1. List of anchor URL link as an array
  2. Class name when the heading is in view

It took some time for investigation, but since Gatsby tableofContentsconveniently added anchor already, we actually need to remove the # from the provided object. Here’s simple function to call urlvalue in the object and remove the first character.

let url = post.items.map(function(post) {
return post['url'].substring(1)
})

After that let’s import Scrollspy right after the nav and pass the url and add the current class name:

<Scrollspy items={url} currentClassName="is-current" className="toc-list">
{post.items.map(p => (
<li key={p.url}>
<a href={p.url}>{p.title}</a>
</li>
))}
</Scrollspy>

The final file for Toc.jsshould look like this

import React from 'react'
import Scrollspy from 'react-scrollspy'
import './Toc.css'
export default function Toc(props) {
const { post } = props
let url = post.items.map(function(post) {
return post['url'].substring(1)
})
return (
<nav>
<Scrollspy items={url} currentClassName="is-current" className="toc-list">
{post.items.map(p => (
<li key={p.url}>
<a href={p.url}>{p.title}</a>
</li>
))}
</Scrollspy>
</nav>
)
}

That’s it. I have created a minimal repo using Gatsby Starter Blog MDX. Feel free to reference it and raise any issues on Github. You can also preview the demo here.

![[20201211-TOC-Final.gif]]

Here’s some of the errors and questions I faced when developing the Table of Content component using Gatsby.

How to debug all the error using Gatsby Build?

If you have been using gatsby developall along to test the Table of Content component. I highly recommend you to use gatsby buildto test all blog posts before commit. I managed to find one blog post that had a wrong heading hierarchy (h3 before h2 ) which resulted in build error.

How to remove icon next to the headings?

The plugin in gatsby-remark-autolink-headersadd an icon before each headings that it process. According to the documentation, make icon: false under plugin options will remove the icons before anchor headings

/*gatsby-config.js*/
{
resolve: `gatsby-remark-autolink-headers`,
options: {
icon: false,
},
},

How to make scroll smooth?

Install smooth-scroll add the following code to Layout.js or any where that will be loaded on everypage.

if (typeof window !== 'undefined') {
// eslint-disable-next-line global-require
require('smooth-scroll')('a[href*="#"]')
}

What next?

There are still some problems in existing solutions. Like react-scrollspy only recognizes the heading instead of the heading + following paragraphs. So long article may experience (Heading 1 In View -> Out of View -> Heading 2 In View) such problem.

Also, only first level heading is rendered currently. Putting a loop would also be the next step in making the table of content better. ß Last but not least, mobile display is also worth working on, currently the table of content would not be rendered in mobile as a majority of my reader come from desktop. But implementing a floating button with pop up menu would also be a feasible solution.


Matt Hagner

Written by Matt Hagner who lives and works in Minneapolis building silly things. You should follow him on Twitter