Build an Interactive Architecture Diagram
Build a polished, dark-themed architecture diagram with React Flow.
Introduction
30 Second Summary
You know those sleek, interactive architecture diagrams on product websites where you can hover over components and see how everything connects? Those aren't made in Figma. They're built with code.
In this project, you will build a polished, dark-themed interactive architecture diagram using React Flow, Motion, and Tailwind CSS. You'll define your architecture as data, style it with custom components, and add hover animations that highlight connected services.
What You'll Build
A single-page React app that renders a production-quality interactive architecture diagram with custom-styled nodes, gradient-animated edges, and hover interactions inspired by Linear's design style.
By the end of this project, you'll have:
- A Vite + React project with Tailwind CSS, React Flow, and Motion fully configured and rendering an interactive diagram in the browser.
- Custom node components (service nodes, group containers, external integrations) styled with a dark theme and gradient-animated edges.
- Hover animations that highlight connected components and reveal descriptions, so the diagram feels alive and explorable.
- A click-to-focus interaction that dims unrelated nodes and slides in a detail panel for the selected component.
Are there any prerequisites?
You'll need Node.js (v18 or higher) and VS Code installed on your machine. Basic familiarity with editing code is helpful, but AI assistance is totally fine for this project.
Before We Start
Before We Start...
Scaffold the React Project and Install Dependencies
Before you can build your architecture diagram, you need a React project with the right tools installed. Vite is a modern build tool that spins up a React project in seconds. In this step, you'll scaffold a new project and install React Flow, Motion, and Tailwind CSS so everything is ready for building.
In This Step, Get Ready To:
- Create a new Vite + React project from your terminal.
- Install and configure React Flow, Motion, and Tailwind CSS.
- Verify the development server runs and loads in your browser.
Create a New Vite React Project
Vite provides a scaffolding command that generates a complete React project structure with a single terminal command. This gives you a working app with all the boilerplate already handled.
- Open VS Code.
- Launch the built-in terminal in VS Code by selecting Terminal, then the drop down New Terminal.
- In your new terminal, run the following command to navigate to your Desktop.
macOS
cd ~/Desktop
Windows
cd %USERPROFILE%\\Desktop
Why navigate first?
The next command creates a brand new folder in whatever directory you're currently in. By navigating to your Desktop first, you'll know exactly where to find your project later. Feel free to navigate somewhere else if you'd prefer your project to be stored elsewhere!
- Run the following command to create a new React project called arch-diagram:
npm create vite@latest arch-diagram -- --template react
What does this command do?
npm create vite@latest runs Vite's project scaffolding tool. The --template react flag tells Vite to set up a React JavaScript project with all the configuration files you need. When the command finishes, you'll see a new arch-diagram folder on your Desktop.
Vite may prompt you with "Install with npm and start now?". Select No. You'll install dependencies manually in the next substep because you need to add extra packages first.
I already selected Yes and a server started
No worries. Press Ctrl+C in your terminal to stop the server. Your project files are already created and dependencies are installed.
- Navigate into your new project folder:
cd arch-diagram
- Open this folder in VS Code by running:
code .
You should see a project structure in VS Code's file explorer with files like src/App.jsx, vite.config.js, and package.json.
Install Dependencies and Configure Tailwind CSS
Your diagram needs three libraries working together. React Flow handles the diagram canvas, node positioning, and edge routing. Motion (formerly Framer Motion) powers hover animations. Tailwind CSS handles all your styling with utility classes.
- Open the terminal in VS Code (make sure you're inside the arch-diagram folder).
- Install all four packages in one command:
npm install @xyflow/react motion tailwindcss @tailwindcss/vite
What are these packages?
- @xyflow/react is the React Flow library for building node-based diagrams.
- motion is the animation library (the renamed version of Framer Motion).
- tailwindcss and @tailwindcss/vite provide Tailwind CSS v4 with its Vite plugin for fast builds.
Tailwind CSS v4 uses a Vite plugin instead of the older PostCSS setup. You need to register this plugin in your Vite config file and update your CSS entry point.
- Open vite.config.js and replace its contents with the following:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
What changed here?
You imported the @tailwindcss/vite plugin and added tailwindcss() to the plugins array. This tells Vite to process your Tailwind utility classes during development and builds.
Now you need to update your CSS file to load Tailwind and React Flow's styles in the correct order.
- Open src/index.css and replace all of its contents with these two import lines:
@import "tailwindcss";
@import "@xyflow/react/dist/style.css";
Pro tip: import order matters
React Flow's styles must come after the Tailwind import. If you reverse the order, Tailwind's reset styles can override React Flow's built-in styling and break the diagram's appearance.
Start the Development Server
With everything installed and configured, you can start the Vite development server to confirm your project runs correctly.
- Run the development server:
npm run dev
- Open your browser and navigate to http://localhost:5173.
I see the Vite + React page
You should see the default Vite React starter page with the Vite and React logos and a counter button. This confirms your project is set up correctly and ready for the next step.
I see an error
Having trouble?
Double-check that you're running npm run dev from inside the arch-diagram folder. If you see a Tailwind-related error, make sure your vite.config.js matches the code above exactly. Try stopping the server with Ctrl+C and running npm run dev again.
Still stuck? Describe your error and I'll help you debug it.
Your project is scaffolded and all your tools are in place. Next up, you'll replace this default page with an actual React Flow diagram and start defining your architecture as data.
Render a Basic React Flow Diagram
Your React project is running, but it's still showing the default Vite welcome page. Time to replace that with something far more interesting. In this step, you'll set up React Flow to render an interactive diagram canvas, define your architecture as data, and see labeled nodes connected by edges in the browser.
In This Step, Get Ready To:
- Set up a React Flow canvas that fills the entire browser viewport.
- Define your architecture as arrays of nodes and edges in a dedicated data file.
- Wire up interactivity so you can drag nodes and pan around the diagram.
Set Up the React Flow Canvas
React Flow needs two things to render: a container element with an explicit width and height, and the ReactFlow component itself. Without a sized container, the canvas collapses to zero pixels and nothing appears.
- Open src/App.jsx in VS Code.
- Delete all of the existing code and replace it with the following:
import { ReactFlow, Background, Controls } from '@xyflow/react';
export default function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
What does this code do?
The outer div uses 100vw and 100vh to fill the entire browser window. Inside it, ReactFlow renders the interactive canvas. Background adds a subtle dot grid, and Controls adds zoom-in/zoom-out buttons in the corner.
Now you need to make sure React Flow's styles are loaded correctly. With Tailwind CSS v4, the import order in your CSS file matters.
- Open src/index.css.
- Delete everything in the file and replace it with the following two imports:
@import "tailwindcss";
@import "@xyflow/react/dist/style.css";
Pro tip: import order matters
The React Flow stylesheet must come after the Tailwind import. If you put it first, Tailwind's reset styles can override React Flow's built-in styling and break the canvas layout.
- Also open src/App.css and delete all of its contents. The default Vite styles will interfere with your diagram layout.
- Save all files and check your browser at http://localhost:5173.
You should see an empty canvas with a dot grid background and zoom controls in the bottom-left corner. No nodes yet, but the canvas is ready.
Define Your Architecture as Data
One of the most powerful things about React Flow is that your entire diagram is defined as plain JavaScript data. Nodes are objects in an array. Edges are objects in another array. This means you can swap out the architecture you're diagramming just by changing a single file.
- Create a new folder called data inside the src folder.
- Create a new file called architecture.js inside the data folder.
- Add the following code to define a basic architecture with five nodes and four edges:
// Each node needs a unique id, a position (x, y), and a data object with a label.
export const initialNodes = [
{
id: 'users',
position: { x: 250, y: 0 },
data: { label: 'Users' },
},
{
id: 'api-gateway',
position: { x: 250, y: 120 },
data: { label: 'API Gateway' },
},
{
id: 'auth',
position: { x: 50, y: 250 },
data: { label: 'Auth Service' },
},
{
id: 'backend',
position: { x: 450, y: 250 },
data: { label: 'Backend Service' },
},
{
id: 'database',
position: { x: 250, y: 400 },
data: { label: 'Database' },
},
];
// Each edge connects a source node to a target node by their ids.
export const initialEdges = [
{ id: 'users-api', source: 'users', target: 'api-gateway' },
{ id: 'api-auth', source: 'api-gateway', target: 'auth' },
{ id: 'api-backend', source: 'api-gateway', target: 'backend' },
{ id: 'backend-db', source: 'backend', target: 'database' },
];
What is a node? What is an edge?
In diagram terminology, a node is a box on the canvas (like "API Gateway" or "Database"). An edge is a line connecting two nodes. Together, they describe the structure of your system. React Flow uses position with absolute pixel coordinates to place each node on the canvas.
Connect the Data and Enable Interactivity
Right now your canvas is empty because the ReactFlow component doesn't know about your nodes and edges. You also need to wire up React Flow's state hooks so that nodes become draggable.
- Open src/App.jsx and replace its contents with the following updated code:
import { ReactFlow, Background, Controls, useNodesState, useEdgesState } from '@xyflow/react';
import { initialNodes, initialEdges } from './data/architecture';
export default function App() {
// These hooks manage node/edge state and enable drag-and-drop
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
fitView
>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
What does this code do?
The useNodesState and useEdgesState hooks are React Flow helpers that manage the diagram's state. They return the current data, a setter function, and a change handler. The change handlers (onNodesChange and onEdgesChange) are what make nodes draggable and selectable.
The fitView prop tells React Flow to automatically zoom and pan so all your nodes are visible when the page loads.
- Save the file and check your browser.
You should see five labeled nodes connected by edges. Try dragging a node around. Try scrolling to zoom in and out. Try clicking and dragging the background to pan. All of this interactivity comes from React Flow's built-in state management.
Seeing a blank page or no nodes?
Make sure your src/index.css file has the React Flow stylesheet import (@import "@xyflow/react/dist/style.css";) after the Tailwind import. Also confirm that src/App.css is empty. If you're still stuck, help me debug my blank React Flow canvas.
Your diagram is interactive and data-driven. The nodes are functional, but they look like plain white boxes. Next up, you'll create custom node components with dark-themed styling to make this diagram look production-quality.
Create Custom Styled Nodes
Your diagram works, but every node looks the same. A polished architecture diagram needs visual hierarchy. Service nodes should look different from group containers, and each node should display an icon and description.
React Flow's custom node system lets you replace the default node appearance with any React component you want. You'll create two custom components: a ServiceNode for individual services and a GroupNode for containers that hold related services together.
In This Step, Get Ready To:
- Build a custom ServiceNode component with dark-themed card styling.
- Build a GroupNode component that acts as a visual container for related services.
- Register both custom node types and expand your architecture data to a full SaaS layout.
Build the ServiceNode Component
Each service in your architecture (API Gateway, Database, Auth) needs its own styled card. React Flow custom nodes are regular React components that receive the node's data object as a prop. You also need to add Handle components from React Flow so edges can connect to the node.
- Create a new folder called components inside your src directory.
- Create a new file called ServiceNode.jsx inside src/components/ and add the following code:
import { Handle, Position } from '@xyflow/react';
export default function ServiceNode({ data }) {
return (
<div className="bg-zinc-900 border border-zinc-700 rounded-xl px-4 py-3 shadow-lg min-w-[160px]">
<Handle type="target" position={Position.Top} className="!bg-violet-500 !w-2 !h-2" />
<div className="flex items-center gap-3">
<div className="text-2xl">{data.icon || '⚙️'}</div>
<div>
<div className="text-sm font-semibold text-zinc-100">{data.label}</div>
<div className="text-xs text-zinc-400">{data.subtitle || ''}</div>
</div>
</div>
<Handle type="source" position={Position.Bottom} className="!bg-violet-500 !w-2 !h-2" />
</div>
);
}
What does this component do?
This component renders each service as a dark card with an icon, title, and subtitle. The Handle components at the top and bottom are the connection points where edges attach. Position.Top creates an incoming connection point, and Position.Bottom creates an outgoing one.
The data prop is whatever you put in the data field of each node in your architecture.js file. You'll add icon and subtitle fields to each node's data shortly.
Build the GroupNode Component
Architecture diagrams group related services together visually. Your GroupNode will render as a large, semi-transparent container with a label at the top. Other nodes will sit inside it by referencing the group's ID with the parentId property.
- Create a new file called GroupNode.jsx inside src/components/ and add the following code:
export default function GroupNode({ data }) {
return (
<div className="bg-zinc-800/40 border border-zinc-700/50 rounded-2xl p-4 min-w-[300px] min-h-[200px]">
<div className="text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2">
{data.label}
</div>
</div>
);
}
Why is the GroupNode so simple?
The GroupNode only renders a labeled container. It doesn't need to render its children. React Flow automatically places child nodes inside the group when those children have a parentId matching the group node's id. Child node positions become relative to the parent's top-left corner.
Register Custom Nodes and Expand the Architecture Data
React Flow needs to know about your custom node types before it can render them. You register them by creating a nodeTypes object that maps type names to components. This object must be defined outside your component (or memoized) so React Flow doesn't re-register types on every render.
- Open App.jsx and add the imports for your custom node components at the top of the file:
import ServiceNode from './components/ServiceNode';
import GroupNode from './components/GroupNode';
- Define the nodeTypes object outside your App component (above the function declaration):
// Define outside the component to prevent re-registration on re-renders
const nodeTypes = {
service: ServiceNode,
group: GroupNode,
};
- Pass the nodeTypes object to your ReactFlow component as a prop:
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
fitView
>
<Background />
<Controls />
</ReactFlow>
What does nodeTypes do?
When a node in your data has type: 'service', React Flow looks up 'service' in the nodeTypes object and renders your ServiceNode component instead of the default node. The same applies to type: 'group' and your GroupNode.
Now update your architecture data to use the full sample architecture with groups and custom node types.
- Replace the contents of src/data/architecture.js with the full architecture data:
export const initialNodes = [
// Group nodes (containers)
{
id: 'external',
type: 'group',
position: { x: 0, y: 0 },
data: { label: 'External' },
style: { width: 380, height: 200 },
},
{
id: 'core',
type: 'group',
position: { x: 0, y: 260 },
data: { label: 'Core Platform' },
style: { width: 580, height: 200 },
},
{
id: 'data-layer',
type: 'group',
position: { x: 0, y: 520 },
data: { label: 'Data Layer' },
style: { width: 380, height: 200 },
},
// External group children
{
id: 'users',
type: 'service',
position: { x: 30, y: 50 },
parentId: 'external',
data: { label: 'Users', subtitle: 'Web & Mobile', icon: '👤' },
},
{
id: 'webhooks',
type: 'service',
position: { x: 210, y: 50 },
parentId: 'external',
data: { label: 'Webhooks', subtitle: 'External Events', icon: '🔗' },
},
// Core Platform group children
{
id: 'auth',
type: 'service',
position: { x: 30, y: 50 },
parentId: 'core',
data: { label: 'Auth Service', subtitle: 'JWT & OAuth', icon: '🔐' },
},
{
id: 'api',
type: 'service',
position: { x: 220, y: 50 },
parentId: 'core',
data: { label: 'API Gateway', subtitle: 'REST & GraphQL', icon: '🌐' },
},
{
id: 'workers',
type: 'service',
position: { x: 410, y: 50 },
parentId: 'core',
data: { label: 'Workers', subtitle: 'Background Jobs', icon: '⚡' },
},
// Data Layer group children
{
id: 'database',
type: 'service',
position: { x: 30, y: 50 },
parentId: 'data-layer',
data: { label: 'PostgreSQL', subtitle: 'Primary Database', icon: '🗄️' },
},
{
id: 'cache',
type: 'service',
position: { x: 210, y: 50 },
parentId: 'data-layer',
data: { label: 'Redis', subtitle: 'Cache Layer', icon: '⚡' },
},
// Standalone integration nodes
{
id: 'payments',
type: 'service',
position: { x: 650, y: 300 },
data: { label: 'Payments', subtitle: 'Stripe', icon: '💳' },
},
{
id: 'email',
type: 'service',
position: { x: 650, y: 440 },
data: { label: 'Email', subtitle: 'SendGrid', icon: '📧' },
},
];
export const initialEdges = [
{ id: 'e-users-auth', source: 'users', target: 'auth' },
{ id: 'e-users-api', source: 'users', target: 'api' },
{ id: 'e-webhooks-api', source: 'webhooks', target: 'api' },
{ id: 'e-auth-api', source: 'auth', target: 'api' },
{ id: 'e-api-workers', source: 'api', target: 'workers' },
{ id: 'e-api-db', source: 'api', target: 'database' },
{ id: 'e-api-cache', source: 'api', target: 'cache' },
{ id: 'e-workers-db', source: 'workers', target: 'database' },
{ id: 'e-api-payments', source: 'api', target: 'payments' },
{ id: 'e-workers-email', source: 'workers', target: 'email' },
];
How does node nesting work?
Notice how child nodes like users have a parentId: 'external' property. This tells React Flow to render that node inside the external group. The child's position becomes relative to the parent's top-left corner, not the canvas.
The group nodes also have a style property with explicit width and height values. This sets the container size so child nodes fit inside.
- Save all files and check your browser.
You should see your architecture diagram with dark-styled service cards nested inside labeled group containers. The standalone Payments and Email nodes should appear to the right, outside any group. All nodes are connected by edges.
Nodes overlapping or not visible?
If child nodes appear outside their groups, make sure the parentId values match the group node's id exactly. Also make sure group nodes are defined before their children in the array. If nodes are too cramped, adjust the style.width and style.height on the group nodes, or tweak child position values.
If nothing renders at all, help me debug my React Flow custom nodes.
Your diagram now has real visual structure. Services have distinct styled cards, and related components are grouped together in labeled containers. Next up, you'll add gradient-styled edges and a dark theme to give the whole diagram a polished, Linear-inspired look.
Style Edges and Apply the Dark Theme
Your custom nodes are rendering, but the diagram still looks like a wireframe. The edges connecting your nodes are plain gray lines, and the bright white background clashes with the polished aesthetic you're aiming for. In this step, you'll build custom gradient edges and apply a dark theme that transforms the entire diagram into something that looks like it belongs on a company's landing page.
In This Step, Get Ready To:
- Create a custom gradient edge component with an SVG linear gradient stroke.
- Apply a dark color mode to the diagram and page background.
- Add a styled title and subtitle above the diagram and fine-tune the layout.
Create a Custom Gradient Edge Component
React Flow lets you define custom edge components the same way you defined custom nodes. Instead of the default gray lines, you'll render edges with an SVG linear gradient that transitions from purple to blue. This is the technique that gives diagrams like Linear's their signature polished look.
- Create a new file at src/components/GradientEdge.jsx.
- Add the following code:
import { BaseEdge, getBezierPath } from '@xyflow/react';
export default function GradientEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
}) {
// Calculate the bezier curve path between source and target nodes
const [edgePath] = getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});
// Each edge needs a unique gradient ID so gradients don't conflict
const gradientId = `gradient-${id}`;
return (
<>
{/* Define the SVG gradient inline */}
<defs>
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#3b82f6" />
</linearGradient>
</defs>
{/* Render the edge path with the gradient stroke */}
<BaseEdge
path={edgePath}
style={{
...style,
stroke: `url(#${gradientId})`,
strokeWidth: 2,
}}
/>
</>
);
}
What Does This Component Do?
React Flow passes your custom edge component the coordinates of the source and target nodes. You use getBezierPath to calculate a smooth curved path between them. The BaseEdge component renders the actual SVG path, and you override its stroke color with an SVG linear gradient that fades from purple (#8b5cf6) to blue (#3b82f6).
Now you need to register this custom edge type and tell your edges to use it.
- Open App.jsx.
- Import your new component at the top of the file:
import GradientEdge from './components/GradientEdge';
- Create an edgeTypes object outside your component function, just like you did with nodeTypes:
const edgeTypes = {
gradient: GradientEdge,
};
- Pass edgeTypes={edgeTypes} as a prop to your ReactFlow component.
- Open data/architecture.js and update every edge to use the gradient type by adding type: 'gradient' to each edge object:
// Example edge with gradient type
{ id: 'e-users-auth', source: 'users', target: 'auth', type: 'gradient' },
{ id: 'e-auth-api', source: 'auth', target: 'api', type: 'gradient' },
// ... update all your edges the same way
Save your files and check the browser. Your edges should now display as smooth purple-to-blue gradient lines instead of plain gray.
Edges Still Showing as Gray?
Make sure every edge in your initialEdges array has type: 'gradient' set. Also confirm that the edgeTypes object is defined outside your component function, not inside it. If it's inside, React recreates it on every render and React Flow can't match the types. Help me debug my gradient edges.
Apply the Dark Theme to the Diagram
React Flow has a built-in dark color mode that reskins its controls and background. Combined with Tailwind CSS classes on your page and nodes, you can transform the entire diagram into a sleek dark-themed interface.
- Open App.jsx and add colorMode="dark" as a prop on your ReactFlow component.
- Update the outer container div that wraps your ReactFlow component to use a dark background with Tailwind classes like bg-gray-950.
- Update the Background component inside ReactFlow to use a subtle dot grid pattern. Set variant="dots", gap={20}, and size={1} to keep the dots small and unobtrusive.
Your ReactFlow component should now look something like this:
<div className="w-screen h-screen bg-gray-950">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
colorMode="dark"
fitView
>
<Background variant="dots" gap={20} size={1} />
<Controls />
</ReactFlow>
</div>
Now update your custom node styles to match the dark theme.
- Open ServiceNode.jsx and update the outer div's Tailwind classes to use dark backgrounds, light text, and subtle borders. For example: bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-3 shadow-lg.
- Update the subtitle text to use a muted color like text-gray-400.
- Open GroupNode.jsx and update its container to use a semi-transparent dark background like bg-gray-800/50 border border-gray-700 rounded-xl. Update the label header text to use text-gray-300.
Save all your files and check the browser. The entire diagram should now have a cohesive dark aesthetic with gradient edges glowing against the dark background.
Why Dark Theme for Diagrams?
Dark backgrounds make colored elements like gradient edges and icons pop visually. Companies like Linear and Vercel use dark themes for their architecture diagrams because the contrast draws attention to the connections and structure rather than the background. The gradient edges you built are barely visible on a white background, but they glow on dark gray.
Add a Title and Fine-Tune the Layout
A polished diagram needs a title to give viewers context. You'll add a heading above the diagram and adjust your node positions so the layout feels intentional rather than random.
- Open App.jsx and add a title section above the ReactFlow container. Restructure your JSX so the outer wrapper holds both the title and the diagram:
<div className="w-screen h-screen bg-gray-950 flex flex-col">
{/* Title section */}
<div className="px-8 pt-6 pb-2">
<h1 className="text-2xl font-bold text-white">System Architecture</h1>
<p className="text-gray-400 text-sm mt-1">Acme SaaS Platform</p>
</div>
{/* Diagram takes remaining space */}
<div className="flex-1">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
colorMode="dark"
fitView
>
<Background variant="dots" gap={20} size={1} />
<Controls />
</ReactFlow>
</div>
</div>
What Does This Layout Do?
The flex flex-col on the outer wrapper stacks the title and diagram vertically. The title section has fixed padding, and flex-1 on the diagram container tells it to fill all remaining vertical space. React Flow requires its parent container to have explicit dimensions, and flex-1 satisfies that requirement.
- Open data/architecture.js and adjust the position values on your nodes so they form a clean, readable layout.
Pro tip: layout strategy
React Flow uses absolute pixel positions. A good approach is to arrange your groups in columns. Place the External group on the left (x: 0), Core Platform in the center (x: 400), and Data Layer on the right (x: 800). Child nodes use positions relative to their parent group, so start them at small offsets like { x: 20, y: 50 }. The fitView prop auto-zooms to fit everything, so focus on relative spacing rather than exact pixel values.
- Save your files and verify the diagram looks clean in the browser. Drag nodes around to test different arrangements, then update the position values in your data file to match the layout you like best.
Your diagram now has a cohesive dark theme with a professional title, gradient edges, and a well-organized layout. Next up, you'll bring it to life with hover animations that highlight connections between nodes.
Add Hover Animations with Motion
Your diagram looks polished with custom nodes, gradient edges, and a dark theme. But it still feels static. Real production diagrams respond to user interaction, letting viewers explore connections by hovering over components. In this step, you'll bring your diagram to life with Motion animations that make nodes glow on hover, dim unrelated components, and reveal descriptions with smooth transitions.
In This Step, Get Ready To:
- Add a hover glow animation to service nodes using Motion's whileHover prop.
- Track hover state to dim unconnected nodes and highlight connections.
- Display an animated tooltip with node descriptions using AnimatePresence.
Wrap Service Nodes with Motion Hover Effects
Motion's motion.div component is a drop-in replacement for a regular div that supports animation props. By wrapping your node's outer container with it, you can add hover effects with a single prop instead of writing CSS keyframes.
- Open components/ServiceNode.jsx.
- Add the Motion import at the top of the file:
import { motion } from "motion/react";
- Replace the outermost <div> wrapping your node card with a <motion.div> and add hover animation props:
<motion.div
whileHover={{
scale: 1.03,
boxShadow: "0 0 20px rgba(139, 92, 246, 0.3)",
}}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="bg-zinc-900 border border-zinc-700 rounded-xl p-4 shadow-lg min-w-[160px]"
>
{/* Keep your existing Handle components and node content here */}
</motion.div>
What Does This Code Do?
The whileHover prop tells Motion to animate to those values when the user hovers over the element. scale: 1.03 makes the node grow slightly. The boxShadow adds a purple glow effect around the card. The spring transition makes the animation feel natural and bouncy instead of robotic.
- Save the file and hover over your service nodes in the browser.
Each node should subtly scale up and emit a soft purple glow when your cursor enters it. The effect reverses smoothly when you move away.
Nodes Not Animating?
Make sure your import path is "motion/react" and not "framer-motion". The library was renamed and the import path changed. Also confirm you installed the motion package (not framer-motion) back in Step 1. Help me debug my Motion import.
Track Hover State to Dim Unconnected Nodes
A glow effect is nice, but the real power of interactive diagrams is showing relationships. When you hover over a node, every unrelated node should fade out so the hovered node and its direct connections stand out. This requires tracking which node is currently hovered at the diagram level, then passing that information down to each node.
- Open App.jsx.
- Add a useState hook to track the hovered node ID:
const [hoveredNode, setHoveredNode] = useState(null);
- Add onNodeMouseEnter and onNodeMouseLeave event handlers to your ReactFlow component:
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodeMouseEnter={(event, node) => setHoveredNode(node.id)}
onNodeMouseLeave={() => setHoveredNode(null)}
colorMode="dark"
fitView
>
<Background />
<Controls />
</ReactFlow>
What Does This Code Do?
React Flow fires onNodeMouseEnter whenever the cursor enters a node, passing the node object. You store that node's ID in state. When the cursor leaves, you clear the state back to null. This gives you a single source of truth for which node is currently hovered.
Now you need to figure out which nodes are connected to the hovered node, and dim everything else. You'll do this by computing a set of connected node IDs from your edges data, then updating each node's style based on whether it's in that set.
- Add a helper function and a useMemo hook that computes which nodes should be highlighted. Place this above your return statement in App.jsx:
// Find all node IDs directly connected to the hovered node
const connectedNodeIds = useMemo(() => {
if (!hoveredNode) return new Set();
const connected = new Set([hoveredNode]);
edges.forEach((edge) => {
if (edge.source === hoveredNode) connected.add(edge.target);
if (edge.target === hoveredNode) connected.add(edge.source);
});
return connected;
}, [hoveredNode, edges]);
// Apply dimming styles to nodes that aren't connected
const styledNodes = nodes.map((node) => ({
...node,
style: {
...node.style,
opacity: hoveredNode && !connectedNodeIds.has(node.id) ? 0.3 : 1,
transition: "opacity 0.2s ease",
},
}));
What Does This Code Do?
The useMemo hook builds a Set of node IDs that are directly connected to the hovered node by scanning the edges array. Then styledNodes maps over every node and sets its opacity to 0.3 if it's not in the connected set. The CSS transition makes the dimming feel smooth instead of instant.
- Update your ReactFlow component to use styledNodes instead of nodes:
<ReactFlow
nodes={styledNodes}
edges={edges}
// ... rest of your props
>
- Make sure you import useMemo from React at the top of App.jsx alongside useState.
- Save the file and test it in the browser.
When you hover over a node, all unrelated nodes should fade to 30% opacity. The hovered node and its direct neighbors stay fully visible. Moving your cursor away restores everything.
Add an Animated Tooltip on Hover
Dimming unrelated nodes is great for showing relationships. But you can take it further by showing a description tooltip when a node is hovered. Motion's AnimatePresence component handles enter and exit animations for elements that are conditionally rendered. This means your tooltip can smoothly fade in when it appears and fade out when it disappears.
- First, add a description field to each node's data in your data/architecture.js file:
// Example: update each node in your initialNodes array
{
id: 'api',
type: 'service',
position: { x: 300, y: 150 },
data: {
label: 'API Gateway',
icon: '🔀',
subtitle: 'Core Platform',
description: 'Routes incoming requests to the appropriate service and handles rate limiting.',
},
parentId: 'core-group',
},
Pro tip: keep descriptions short
One sentence per node is ideal. The tooltip should give a quick overview, not a full explanation. Think of it like a hover card on a dashboard.
- Add descriptions to all of your service nodes. You can skip group nodes since they serve as visual containers rather than individual components.
- Open components/ServiceNode.jsx and add the AnimatePresence import:
import { motion, AnimatePresence } from "motion/react";
- Add a local hover state and the animated tooltip inside your ServiceNode component. Place the tooltip below the existing node content, inside the <motion.div> wrapper:
import { useState } from 'react';
import { Handle, Position } from '@xyflow/react';
import { motion, AnimatePresence } from 'motion/react';
export default function ServiceNode({ data }) {
const [isHovered, setIsHovered] = useState(false);
return (
<motion.div
whileHover={{
scale: 1.03,
boxShadow: '0 0 20px rgba(139, 92, 246, 0.3)',
}}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
className="bg-zinc-900 border border-zinc-700 rounded-xl p-4 shadow-lg min-w-[160px]"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Handle type="target" position={Position.Top} className="opacity-0" />
{/* Node content */}
<div className="flex items-center gap-2 mb-1">
<span className="text-lg">{data.icon}</span>
<span className="text-white font-medium text-sm">{data.label}</span>
</div>
{data.subtitle && (
<span className="text-zinc-400 text-xs">{data.subtitle}</span>
)}
{/* Animated tooltip */}
<AnimatePresence>
{isHovered && data.description && (
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: 0.15 }}
className="mt-3 pt-3 border-t border-zinc-700"
>
<p className="text-zinc-300 text-xs leading-relaxed">
{data.description}
</p>
</motion.div>
)}
</AnimatePresence>
<Handle type="source" position={Position.Bottom} className="opacity-0" />
</motion.div>
);
}
What Does This Code Do?
The AnimatePresence component watches its children. When a child mounts (hover starts), it plays the animate transition. When a child unmounts (hover ends), it plays the exit transition before removing it from the DOM. The tooltip fades in from 5 pixels below and fades out the same way.
- Save the file and hover over different service nodes in the browser.
You should see three things happening at once: the node scales up with a purple glow, unrelated nodes dim to 30% opacity, and a description tooltip smoothly fades in below the node's label. Moving your cursor away reverses all three effects.
Your architecture diagram now responds to user interaction with professional-quality animations. Next up, you'll take this even further with a click-to-focus mode in the Secret Mission.
Secret mission
Add Click-to-Focus with Connection Highlighting
Your hover interactions are great, but what if users could click a node to lock focus on it? In this secret mission, you'll build a click-to-focus mode that dims unrelated nodes, brightens connected edges, and slides in a detail panel from the side.
Before you go
This project runs entirely locally on your machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Vite development server (running on http://localhost:5173).
- The arch-diagram project folder with all source files.
- npm packages installed locally (@xyflow/react, motion, tailwindcss, @tailwindcss/vite) in the node_modules folder.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with your diagram.
- Your Vite dev server will keep running until you close the terminal or press Ctrl + C.
- Your project files stay in the arch-diagram folder, ready for you to modify anytime.
Pause - I'll come back to this later
Shut down the running dev server to free up memory, but keep your project files for later.
- Press Ctrl + C in the terminal where the Vite dev server is running to stop it.
- Your project files and installed packages remain in the arch-diagram folder.
- To resume later, open the folder in VS Code, open a terminal, and run npm run dev.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Press Ctrl + C in the terminal to stop the Vite dev server if it is still running.
- Navigate to the parent directory of your project folder by running cd .. in your terminal.
- Delete the entire project folder by running rm -rf arch-diagram on macOS/Linux or rmdir /s /q arch-diagram on Windows.
- This removes all source files, the node_modules folder, and all installed packages.
Mission accomplished
Nice work! You just built a production-quality interactive architecture diagram entirely from code. This is the same approach companies like Linear and Stripe use to create polished, interactive diagrams for their websites and documentation.
Here's what you learned:
- Defined an architecture as data using nodes and edges arrays, then rendered it as a fully interactive diagram with React Flow.
- Created custom node components (ServiceNode and GroupNode) with polished dark-theme styling using Tailwind CSS and custom gradient edges for a Linear-inspired visual style.
- Added hover animations with Motion that highlight connected components and reveal node details with smooth transitions.
- Built a click-to-focus interaction that dims unrelated nodes, brightens connected edges, and slides in a detail panel for the selected component.
Ready to quiz yourself?