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:
Hope you enjoy it!
Stack
I'd like to try Bun this time:
- Bun — A fast all-in-one JavaScript runtime
- Optional: Configure git to properly display diff for bun's lockfile:
Modules
framer-motion
for animations.zustand
for state management.kuma-ui
for building UI componentsprettier
for formtting codemodern-normalize
for normalizing browsers' default style
❯ 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: