
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:
- Server Component: Fetches the mind map data (nodes, edges, relationships)
- Client Component: Renders and animates everything with react-flow and motion/react
Part 1: Getting setup
First, let's install our tools:
npm install @xyflow/react motionNote: 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.
1 // app/mind-map/page.tsx 2 import MindMapClient from "./MindMapClient"; 3 import { Node, Edge } from "@xyflow/react"; 4 5 type NodeData = { label: string; color?: string }; 6 type MindMapNode = Node<NodeData, "animated" | "static">; 7 8 async 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 65 export 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:
1 "use client"; 2 3 import { 4 ReactFlow, 5 Node, 6 Edge, 7 Controls, 8 Background, 9 BackgroundVariant, 10 Position, 11 Handle, 12 } from "@xyflow/react"; 13 import "@xyflow/react/dist/style.css"; 14 import { 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.
1 type NodeData = { label: string; color?: string }; 2 type 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.
1 function 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:
1 function 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:
1 const nodeTypes = { 2 animated: AnimatedNode, 3 static: StaticNode, 4 };
Step 6: Bring It All Together
Finally, render the ReactFlow component with your nodes and edges:
1 export 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:
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:
1 // Minimalist style 2 className="relative px-4 py-2 rounded-md border border-gray-300 bg-white shadow-sm" 3 4 // Gradient style 5 className="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 8 className="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?
1 async 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.
1 "use client"; 2 3 import { 4 ReactFlow, 5 Node, 6 Edge, 7 Controls, 8 Background, 9 BackgroundVariant, 10 Position, 11 Handle, 12 } from "@xyflow/react"; 13 import "@xyflow/react/dist/style.css"; 14 import { motion } from "motion/react"; 15 16 type NodeData = { label: string; color?: string }; 17 type MindMapNode = Node<NodeData, "animated" | "static">; 18 19 // Custom animated node component with neobrutalist styling 20 function 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 57 function 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 82 const nodeTypes = { 83 animated: AnimatedNode, 84 static: StaticNode, 85 }; 86 87 const 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 138 const 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 213 export 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 }