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:
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.
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]]
![[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]]
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 } = propsconsole.log(post)return <h3>Table of Content</h3>}
Save file and back to blog-post.js
and 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]]
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 tableofContents
from 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-headers
to 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]]
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.css
in 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-current
class 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;}
Last but not least, we will need to detect if the heading is in view. react-scrollspy
will help us to identify if the anchor is in view after we pass through 2 things.
It took some time for investigation, but since Gatsby tableofContents
conveniently added anchor already, we actually need to remove the #
from the provided object. Here’s simple function to call url
value 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.js
should look like this
import React from 'react'import Scrollspy from 'react-scrollspy'import './Toc.css'export default function Toc(props) {const { post } = propslet 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.
If you have been using gatsby develop
all along to test the Table of Content component. I highly recommend you to use gatsby build
to 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.
The plugin in gatsby-remark-autolink-headers
add 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,},},
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-requirerequire('smooth-scroll')('a[href*="#"]')}
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.
Written by Matt Hagner who lives and works in Minneapolis building silly things. You should follow him on Twitter