Inksh logoContactAbout usPrivacy
Author name Hemant Bhatt

1st November 2025

Animated Mind Maps in Next.js: Key to explaining visually

👇 Try It Out! Drag the nodes, zoom, and explore

This is what we're building. A fully interactive, animated mind map that you can pan, zoom, and move. Go ahead, try it!

Introduction

Ever stared at a block of mundane, boring text and thought, "I wish this was a colorful, bouncing diagram instead"? Time to embrace mindmaps. Here ideas sprawl like a developer's browser tabs at 2 AM.

In this guide, we'll build animated mind maps in Next.js using the App Router.

The Two-Part Architecture

Building mind maps in Next.js App Router follows a simple pattern:

Part 1: Getting setup

First, let's install our tools:

terminal
npm install @xyflow/react motion

Note: We're using @xyflow/react (ReactFlow 12) and motion/react (the newer, shinier library) instead of their older counterparts.

Part 2: The Server Component (Data Fetching)

Understanding the Data Structure

Before we dive into the code, let's understand the two core data structures:

MindMapNode

Each node in your mind map represents a concept or idea. The structure includes:

  • id: Unique identifier for the node
  • type: Either "animated" or "static" to determine which node component renderer to use
  • position: Object with x and y coordinates for placement on the canvas
  • data: Object containing the label (text displayed) and any custom properties—styling comes from our single custom node renderer

MindMapEdge

Edges define the relationships between nodes - the connecting lines that show how ideas relate:

  • id: Unique identifier for the edge
  • source: ID of the node where the edge starts
  • target: ID of the node where the edge ends
  • animated: Optional boolean to make the edge flow with animation

Here we are simulating the data fetching of the mind map. The getMindMapData() function can be a database, CMS or an API call.

app/mind-map/page.tsx
1// app/mind-map/page.tsx
2import MindMapClient from "./MindMapClient";
3import { Node, Edge } from "@xyflow/react";
4
5type NodeData = { label: string; color?: string };
6type MindMapNode = Node<NodeData, "animated" | "static">;
7
8async function getMindMapData() {
9 // This could be from a database, CMS, or API
10 // For now, let's create a simple structure
11 const nodes: MindMapNode[] = [
12 {
13 id: "1",
14 type: "static",
15 position: { x: 250, y: 0 },
16 data: { label: "Next.js Mind Maps", color: "bg-blue-100" },
17 },
18 {
19 id: "2",
20 type: "static",
21 position: { x: 250, y: 100 },
22 data: { label: "Server Components", color: "bg-green-100" },
23 },
24 {
25 id: "3",
26 type: "static",
27 position: { x: 250, y: 300 },
28 data: { label: "Client Components", color: "bg-purple-100" },
29 },
30 {
31 id: "4",
32 type: "animated",
33 position: { x: 165, y: 500 },
34 data: { label: "Animated Diagrams! 🎉", color: "bg-orange-100" },
35 },
36 ];
37
38 const edges: Edge[] = [
39 {
40 id: "e1-2",
41 source: "1",
42 target: "2",
43 animated: false,
44 style: { stroke: "#000", strokeWidth: 3 },
45 },
46 {
47 id: "e2-3",
48 source: "2",
49 target: "3",
50 animated: true,
51 style: { stroke: "#000", strokeWidth: 3 },
52 },
53 {
54 id: "e3-4",
55 source: "3",
56 target: "4",
57 animated: true,
58 style: { stroke: "#000", strokeWidth: 3 },
59 },
60 ];
61
62 return { nodes, edges };
63}
64
65export default async function MindMapPage() {
66 const { nodes, edges } = await getMindMapData();
67
68 return (
69 <div className="w-full h-screen">
70 <h1 className="text-3xl font-bold p-4">My Animated Mind Map</h1>
71 <MindMapClient initialNodes={nodes} initialEdges={edges} />
72 </div>
73 );
74}

Part 3: The Client Component (Animation Magic)

Now for the fun part. Create a client component that brings everything to life. Let's break it down into digestible pieces:

Step 1: Mark it as a Client Component

First things first—we need browser APIs for animations and interactivity:

app/mind-map/MindMapClient.tsx
1"use client";
2
3import {
4 ReactFlow,
5 Node,
6 Edge,
7 Controls,
8 Background,
9 BackgroundVariant,
10 Position,
11 Handle,
12} from "@xyflow/react";
13import "@xyflow/react/dist/style.css";
14import { motion } from "motion/react";

Step 2: Define Your Types

TypeScript helps us keep our node data structured and predictable, we have 2 types of nodes. Animated and static. We define them below.

app/mind-map/MindMapClient.tsx
1type NodeData = { label: string; color?: string };
2type MindMapNode = Node<NodeData, "animated" | "static">;

Step 3: Create Your Animated Node

This is the code for the pulsating animation node. We use motion.div for the effect.

app/mind-map/MindMapClient.tsx
1function AnimatedNode({ data }: { data: NodeData }) {
2 const bgColor = data.color || "bg-white";
3
4 return (
5 <motion.div
6 initial={{ scale: 0.85, opacity: 0 }}
7 animate={{ scale: [1, 1.2, 1], opacity: 1 }}
8 transition={{
9 opacity: { duration: 0.4, ease: "easeOut" },
10 scale: {
11 duration: 0.6,
12 ease: "easeInOut",
13 repeat: Infinity,
14 repeatDelay: 1.4,
15 repeatType: "loop",
16 },
17 }}
18 className={[
19 "relative px-6 py-3 shadow-[4px_4px_0px_0px_#000] rounded-lg border-4 border-black flex flex-col items-center text-center",
20 bgColor,
21 ].join(" ")}
22 >
23 <Handle
24 type="target"
25 position={Position.Top}
26 className="pointer-events-none opacity-0 -top-3 absolute h-3 w-3"
27 />
28 <div className="font-black text-sm md:text-base">{data.label}</div>
29 <Handle
30 type="source"
31 position={Position.Bottom}
32 className="pointer-events-none opacity-0 -bottom-3 absolute h-3 w-3"
33 />
34 </motion.div>
35 );
36}

Key details: The scale: [1, 1.2, 1] creates a pulse effect that repeats infinitely. The Handle components are invisible but crucial—they're the connection points for edges.

Step 4: Create a Static Node (Optional)

Not everything needs to bounce. Static nodes provide visual contrast:

app/mind-map/MindMapClient.tsx
1function StaticNode({ data }: { data: NodeData }) {
2 const bgColor = data.color || "bg-white";
3
4 return (
5 <div
6 className={[
7 "relative px-6 py-3 shadow-[4px_4px_0px_0px_#000] rounded-lg border-4 border-black flex flex-col items-center text-center",
8 bgColor,
9 ].join(" ")}
10 >
11 <Handle
12 type="target"
13 position={Position.Top}
14 className="pointer-events-none opacity-0 -top-3 absolute h-3 w-3"
15 />
16 <div className="font-black text-sm md:text-base">{data.label}</div>
17 <Handle
18 type="source"
19 position={Position.Bottom}
20 className="pointer-events-none opacity-0 -bottom-3 absolute h-3 w-3"
21 />
22 </div>
23 );
24}

Step 5: Register Your Node Types

Tell ReactFlow about your custom nodes:

app/mind-map/MindMapClient.tsx
1const nodeTypes = {
2 animated: AnimatedNode,
3 static: StaticNode,
4};

Step 6: Bring It All Together

Finally, render the ReactFlow component with your nodes and edges:

app/mind-map/MindMapClient.tsx
1export default function MindMapClient({ initialNodes, initialEdges }) {
2 return (
3 <motion.div
4 initial={{ opacity: 0 }}
5 animate={{ opacity: 1 }}
6 transition={{ duration: 0.5 }}
7 className="w-full h-[500px] md:h-[600px] border-4 border-black shadow-[8px_8px_0px_0px_#000]"
8 >
9 <ReactFlow
10 defaultNodes={initialNodes}
11 defaultEdges={initialEdges}
12 nodeTypes={nodeTypes}
13 fitView
14 minZoom={0.2}
15 nodesConnectable={false}
16 >
17 <Controls className="border-2 border-black shadow-[4px_4px_0px_0px_#000]" />
18 <Background
19 variant={BackgroundVariant.Dots}
20 gap={20}
21 size={2}
22 color="#000"
23 className="opacity-20"
24 />
25 </ReactFlow>
26 </motion.div>
27 );
28}

Pro tip: The fitView prop automatically centers and scales your diagram to fit the viewport. The Controls component adds zoom/pan buttons, and Background adds those satisfying dots.

Part 4: Making It Your Own

The beauty of this setup is customization. Want different animations? Change the motion props:

example
1// Fade and slide from left
2<motion.div
3 initial={{ x: -100, opacity: 0 }}
4 animate={{ x: 0, opacity: 1 }}
5 transition={{ duration: 0.6, ease: 'easeOut' }}
6>

Want different styles? Change the Tailwind classes:

example
1// Minimalist style
2className="relative px-4 py-2 rounded-md border border-gray-300 bg-white shadow-sm"
3
4// Gradient style
5className="relative px-6 py-3 rounded-xl bg-gradient-to-br from-purple-400 to-pink-500 text-white shadow-lg"
6
7// Glassmorphism style
8className="relative px-6 py-3 rounded-2xl bg-white/20 backdrop-blur-md border border-white/30 shadow-xl"

Want to fetch data from an API?

example
1async function getMindMapData() {
2 const res = await fetch("https://api.example.com/mind-maps/123");
3 const data = await res.json();
4 return data;
5}

Want to add interactive features? @xyflow/react gives you drag-and-drop, zoom, pan, and connection creation out of the box. It's like LEGO for diagrams.

Conclusion

The server handles data fetching efficiently, while the client creates smooth, engaging animations. @xyflow/react handles the complex graph logic, and motion/react brings everything to life with butter-smooth animations.

The result? A mind map that doesn't just display information—it performs it. Your users will thank you, and your wall of text you want to get accross will finally look cool.

Under the Hood: The Hero Mind Map Component

Curious how the hero demo works? Here's the exact client component powering it—complete with springy Motion nodes, inksh styling, and the top/bottom handles that make the edges render.

app/blog/next-tutorial/mind-maps/InkshMindMap.tsx
1"use client";
2
3import {
4 ReactFlow,
5 Node,
6 Edge,
7 Controls,
8 Background,
9 BackgroundVariant,
10 Position,
11 Handle,
12} from "@xyflow/react";
13import "@xyflow/react/dist/style.css";
14import { motion } from "motion/react";
15
16type NodeData = { label: string; color?: string };
17type MindMapNode = Node<NodeData, "animated" | "static">;
18
19// Custom animated node component with neobrutalist styling
20function AnimatedNode({ data }: { data: NodeData }) {
21 const bgColor = data.color || "bg-white";
22
23 return (
24 <motion.div
25 initial={{ scale: 0.85, opacity: 0 }}
26 animate={{ scale: [1, 1.2, 1], opacity: 1 }}
27 transition={{
28 opacity: { duration: 0.4, ease: "easeOut" },
29 scale: {
30 duration: 0.6,
31 ease: "easeInOut",
32 repeat: Infinity,
33 repeatDelay: 1.4,
34 repeatType: "loop",
35 },
36 }}
37 className={[
38 "relative px-6 py-3 shadow-[4px_4px_0px_0px_#000] rounded-lg border-4 border-black flex flex-col items-center text-center",
39 bgColor,
40 ].join(" ")}
41 >
42 <Handle
43 type="target"
44 position={Position.Top}
45 className="pointer-events-none opacity-0 -top-3 absolute h-3 w-3"
46 />
47 <div className="font-black text-sm md:text-base">{data.label}</div>
48 <Handle
49 type="source"
50 position={Position.Bottom}
51 className="pointer-events-none opacity-0 -bottom-3 absolute h-3 w-3"
52 />
53 </motion.div>
54 );
55}
56
57function StaticNode({ data }: { data: NodeData }) {
58 const bgColor = data.color || "bg-white";
59
60 return (
61 <div
62 className={[
63 "relative px-6 py-3 shadow-[4px_4px_0px_0px_#000] rounded-lg border-4 border-black flex flex-col items-center text-center",
64 bgColor,
65 ].join(" ")}
66 >
67 <Handle
68 type="target"
69 position={Position.Top}
70 className="pointer-events-none opacity-0 -top-3 absolute h-3 w-3"
71 />
72 <div className="font-black text-sm md:text-base">{data.label}</div>
73 <Handle
74 type="source"
75 position={Position.Bottom}
76 className="pointer-events-none opacity-0 -bottom-3 absolute h-3 w-3"
77 />
78 </div>
79 );
80}
81
82const nodeTypes = {
83 animated: AnimatedNode,
84 static: StaticNode,
85};
86
87const mindMapNodes: MindMapNode[] = [
88 {
89 id: "1",
90 type: "static",
91 position: { x: 250, y: 0 },
92 data: { label: "Next.js Mind Maps", color: "bg-blue-100" },
93 },
94 {
95 id: "2",
96 type: "static",
97 position: { x: 250, y: 100 },
98 data: { label: "Server Components", color: "bg-green-100" },
99 },
100 {
101 id: "4",
102 type: "static",
103 position: { x: 80, y: 200 },
104 data: { label: "Data Fetching", color: "bg-yellow-100" },
105 },
106 {
107 id: "5",
108 type: "static",
109 position: { x: 250, y: 200 },
110 data: { label: "SSR", color: "bg-yellow-100" },
111 },
112 {
113 id: "3",
114 type: "static",
115 position: { x: 250, y: 300 },
116 data: { label: "Client Components", color: "bg-purple-100" },
117 },
118 {
119 id: "6",
120 type: "static",
121 position: { x: 80, y: 400 },
122 data: { label: "@xyflow/react", color: "bg-pink-100" },
123 },
124 {
125 id: "7",
126 type: "static",
127 position: { x: 250, y: 400 },
128 data: { label: "motion/react", color: "bg-pink-100" },
129 },
130 {
131 id: "8",
132 type: "animated",
133 position: { x: 165, y: 500 },
134 data: { label: "Animated Diagrams! 🎉", color: "bg-orange-100" },
135 },
136];
137
138const mindMapEdges: Edge[] = [
139 // First node to Server Components
140 {
141 id: "e1-2",
142 source: "1",
143 target: "2",
144 animated: false,
145 style: { stroke: "#000", strokeWidth: 3 },
146 },
147 // Server Components to Data Fetching
148 {
149 id: "e2-4",
150 source: "2",
151 target: "4",
152 animated: false,
153 style: { stroke: "#000", strokeWidth: 3 },
154 },
155 // Server Components to SSR
156 {
157 id: "e2-5",
158 source: "2",
159 target: "5",
160 animated: false,
161 style: { stroke: "#000", strokeWidth: 3 },
162 },
163 // Data Fetching to Client Components
164 {
165 id: "e4-3",
166 source: "4",
167 target: "3",
168 animated: false,
169 style: { stroke: "#000", strokeWidth: 3 },
170 },
171 // SSR to Client Components
172 {
173 id: "e5-3",
174 source: "5",
175 target: "3",
176 animated: false,
177 style: { stroke: "#000", strokeWidth: 3 },
178 },
179 // Client Components to @xyflow/react
180 {
181 id: "e3-6",
182 source: "3",
183 target: "6",
184 animated: true,
185 style: { stroke: "#000", strokeWidth: 3 },
186 },
187 // Client Components to motion/react
188 {
189 id: "e3-7",
190 source: "3",
191 target: "7",
192 animated: true,
193 style: { stroke: "#000", strokeWidth: 3 },
194 },
195 // @xyflow/react to Animated Diagrams
196 {
197 id: "e6-8",
198 source: "6",
199 target: "8",
200 animated: true,
201 style: { stroke: "#000", strokeWidth: 3 },
202 },
203 // motion/react to Animated Diagrams
204 {
205 id: "e7-8",
206 source: "7",
207 target: "8",
208 animated: true,
209 style: { stroke: "#000", strokeWidth: 3 },
210 },
211];
212
213export default function InkshMindMap() {
214 return (
215 <motion.div
216 initial={{ opacity: 0 }}
217 animate={{ opacity: 1 }}
218 transition={{ duration: 0.5 }}
219 className="w-full h-[500px] md:h-[600px] border-4 border-black shadow-[8px_8px_0px_0px_#000] bg-gradient-to-br from-blue-50 to-purple-50"
220 >
221 <ReactFlow
222 defaultNodes={mindMapNodes}
223 defaultEdges={mindMapEdges}
224 nodeTypes={nodeTypes}
225 fitView
226 minZoom={0.2}
227 nodesConnectable={false}
228 >
229 <Controls className="border-2 border-black shadow-[4px_4px_0px_0px_#000]" />
230 <Background
231 variant={BackgroundVariant.Dots}
232 gap={20}
233 size={2}
234 color="#000"
235 className="opacity-20"
236 />
237 </ReactFlow>
238 </motion.div>
239 );
240}