How to build a smoothly animated table of contents with Framer Motion and Kuma UI

How to build a smoothly animated table of contents with Framer Motion and Kuma UI

Hey, what's up? It's Takuya here. This article would be a supplemental resource for my latest tutorial posted on YouTube:

This is a tutorial on how to accomplish the animation effect for a table of contents that I posted on X here:

The source code can be found here:

GitHub - craftzdog/smooth-toc-example: A demo project of a smoothly animated table of contents
A demo project of a smoothly animated table of contents - craftzdog/smooth-toc-example

Hope you enjoy it!

Stack

I'd like to try Bun this time:

Modules

❯ bun i framer-motion zustand @kuma-ui/core modern-normalize
❯ bun i -d @kuma-ui/vite
❯ bun i -d prettier @ianvs/prettier-plugin-sort-imports

Packages for rendering Markdown content using:

❯ bun i hast mdast rehype-react rehype-slug remark-frontmatter remark-gfm remark-parse remark-rehype unified unist-util-visit yaml

I'm not gonna explain the details about Markdown renderer since it's not the main topic in this tutorial.
But you can learn how to build a Markdown renderer through their docs.
It's pretty extensible and they are highly recommended.

Use Bun to run Vite

I'd like to use Bun to run Vite.

diff --git a/package.json b/package.json
index 051b7b9..706ee9d 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,8 @@
   "version": "0.0.0",
   "type": "module",
   "scripts": {
-    "dev": "vite",
-    "build": "tsc -b && vite build",
+    "dev": "bunx --bun vite",
+    "build": "tsc -b && bunx --bun vite build",
     "lint": "eslint .",
     "preview": "vite preview"
   },

Configure prettier

Inspired by Christoph Nakazawa's setup:

He uses the @ianvs/prettier-plugin-sort-imports plugin.

My .prettierrc.json: Prettier config

Take a look at the importOrder field.
It automatically sorts the import order as documented on the line above, with Node.js built-in modules at the top, followed by react, third party modules, my private Inkdrop modules, and local modules starting with a @ character, any relative import starting with a . character, type definition imports, other modules, and lastly, CSS files.

Learn Zustand

Zustand simplifies state management. Even in TypeScript, you can avoid a lot of redundant lines unlike Redux. Let's replace the basic useState example with Zustand.

diff --git a/src/App.tsx b/src/App.tsx
index 60fe936..b0a957a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,10 +1,20 @@
-import { useState } from 'react'
 import reactLogo from './assets/react.svg'
 import viteLogo from '/vite.svg'
 import './App.css'
+import { create } from 'zustand'
+
+interface CounterState {
+  count: number
+  increment: () => void
+}
+
+const useCounterStore = create<CounterState>(set => ({
+  count: 0,
+  increment: () => set(state => ({ count: state.count + 1 }))
+}))

 function App() {
-  const [count, setCount] = useState(0)
+  const { count, increment } = useCounterStore()

   return (
     <>
@@ -18,9 +28,7 @@ function App() {
       </div>
       <h1>Vite + React</h1>
       <div className="card">
-        <button onClick={() => setCount(count => count + 1)}>
-          count is {count}
-        </button>
+        <button onClick={increment}>count is {count}</button>
         <p>
           Edit <code>src/App.tsx</code> and save to test HMR
         </p>

How to rename App.tsx to app.tsx

git mv -f src/App.tsx src/app.tsx
git mv -f src/App.css src/app.css

Then, edit src/main.tsx

Build UIs

Use CSS variables for tokens

I've been using CSS variables for color palettes these days.
I borrowed the color palette from TailwindCSS.

Render a Markdown doc

Import a raw static file as string in Vite

Assets can be imported as strings using the ?raw suffix like so:

import md from './example.md?raw'

Use Remark to render Markdown as HTML

Too many verbose logs when rendering with Remark?

It's from micromark:

If it happens, suppress it by doing so:

diff --git a/src/main.tsx b/src/main.tsx
index 265ee5c..6f96b9a 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,9 +1,12 @@
 import { StrictMode } from 'react'
 import { createRoot } from 'react-dom/client'
+import debug from 'debug'
 import App from './app.tsx'
 import './index.css'
 import './tokens.css'

+debug.disable()
+
 createRoot(document.getElementById('root')!).render(
   <StrictMode>
     <App />

Create the outline view

Extract headings from the mdast tree

  • github-slugger helps you generate unique heading IDs for rendering Markdown

Fixing a bug caused by impure rendering (StrictMode)

Vite enables StrictMode by default, which helps you find bugs:

Your components will re-render an extra time to find bugs caused by impure rendering.Your components will re-run Effects an extra time to find bugs caused by missing Effect cleanup.

So, it re-renders MarkdownView twice, wihch causes the ref values to be null.

Join our Discord server!

Stay motivated and inspired with like-minded doers. How to join:

Stay motivated and inspired by connecting with other tech note-takers - Inkdrop User Manual
Join our Discord server and get inspired and motivated