SGDG

Hi! My name is SyL 👋
This is where I invite you to explore my journey as a solo entrepreneur. Join me on this adventure 🚀, where I share valuable insights and experiences to empower your entrepreneurial path. 🌟

Add Table of Contents to your Nuxt 3 blog in 10 mins

I'll show you how to add Table of Contents to your Nuxt blog in 10 mins.

Written by
SyL
Published on
08 Oct 2023

Introduction

👋 Hi all, we will be extending our blog to add Table of Contents in our article. We will be using the Nuxt blog we created in this article. You can download the latest source code there and this is what we will accomplish by the end of this article.

Table of Contents

Update the catch all route page

Our article is currently begin shown with the catch-all route page under /pages/[...slug].vue. Let's update it to place our Table of Contents.

//page/[...slug].vue<template>  <main class="my-20 max-w-5xl mx-auto">    <ContentDoc v-slot="{ doc }">      <ArticleDetails :article="doc" class="mb-5" />      <!-- Update code from below -->      <div class="my-8 lg:my-16 flex flex-col gap-10 lg:flex-row-reverse lg:items-start lg:gap-20 lg:px-4">                <ArticleTableOfContents          :toc="doc.body.toc?.links ?? []"          class="top-14 h-auto lg:sticky lg:w-1/4" />        <ContentRenderer :value="doc" class="lg:w-3/4" />      </div>    </ContentDoc>  </main></template>

In our code above we added a new component ArticleTableOfContents and place it next to our article content. We also passed over the doc.body.toc?.links which is a Table of Contents generated by Nuxt Content that contain a list of <h2> and <h3> elements in our article.

✨ We can now begin to construct our Table of Contents

Table of Contents

Let's begin by creating a component ArticleTableOfContents.vue under our components folder and insert the following:

//components/ArticleTableOfContents.vue<script lang="ts" setup>import { TocLink } from "@nuxt/content/dist/runtime/types";// toc variable that contains the <h2> and <h3> tags from our articleconst props = defineProps<{  toc: TocLink[];}>();const activeToc = ref<string | null>(null);function isScrollAtEndOfWindow(): boolean {  return (    window.innerHeight + Math.round(window.scrollY) >=    document.body.offsetHeight - 150  );}function onScrollGetActiveToc() {  const scrollY = window.scrollY;  // take last toc if scrolled to end of window  if (isScrollAtEndOfWindow()) {    activeToc.value = [...props.toc].pop()?.id ?? null;    return;  }  // retrieve the active state of toc  for (const link of [...props.toc].reverse()) {    const el = document.getElementById(link.id);    if (!el) {      continue;    }    if (scrollY >= el.offsetTop - 150) {      activeToc.value = link.id;      return;    }  }}onMounted(() => {  window.addEventListener("scroll", onScrollGetActiveToc);});onUnmounted(() => {  window.removeEventListener("scroll", onScrollGetActiveToc);});</script><template>  <aside>    <div class="mb-2 font-semibold">Table of Contents</div>    <ul class="not-prose m-0 flex list-none flex-col gap-4 p-0">      <li v-for="{ id, text } in toc" :key="id" class="m-0 p-0 leading-tight">        <NuxtLink          class="text-gray-500 no-underline hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300"          :href="`#${id}`"          :title="text"          :class="{ 'font-medium': activeToc == id }"        >          {{ text }}        </NuxtLink>      </li>    </ul>  </aside></template>

Our ArticleTableOfContents.vue component will receive a toc variable that contains the list of <h2> and <h3> elements in our Markdown articles. Using that, we wil find the active content in our onScrollGetActiveToc function.

Getting active state of TOC

Let's look inside the onScrollGetActiveToc function where we get the active state of our Table of Contents so we can highlight it to our viewers. We first check if our viewers is at the end of the article and take the last toc as the active one if so. Otherwise, we look at the offsetTop of each toc content starting from the last and compare them against scrollY.

function onScrollGetActiveToc() {  const scrollY = window.scrollY;  // take last toc if scrolled to end of window  if (isScrollAtEndOfWindow()) {    activeToc.value = [...props.toc].pop()?.id ?? null;    return;  }  // retrieve the active state of toc  for (const link of [...props.toc].reverse()) {    const el = document.getElementById(link.id);    if (!el) {      continue;    }    if (scrollY >= el.offsetTop - 150) {      activeToc.value = link.id;      return;    }  }}

✨ With this, our Table of Contents will hightlight the active toc section when our viewers sroll through our article!

Smooth scrolling

When you click on a link on our Table of Contents, you will notice that a jump to the content. We can easily enable smooth scrolling with just CSS. We can update our app.vue with the following:

//app.vue<template>  <NuxtLayout>    <NuxtPage />  </NuxtLayout></template><style>html {  scroll-behavior: smooth;  /* Add smooth scrolling */}</style>

✨ We have now successfully added Table of Contents with smooth scrolling to our article!

What's next?

Download the complete code here at Github.

Check out more related articles below!

Create a Nuxt 3 Content blog with Tailwind CSS in one hour.
Add dark mode to your Nuxt 3 app in 15 mins.
Deploy your Nuxt 3 app with Vercel for free in 5 mins