Building Interactive Graph Visualizations with Vis.js and Next.js 16
~7 min readaiGraph visualizations have become essential in modern web applications, particularly for AI-powered systems that need to represent complex relationships between entities. However, building interactive graphs that work consistently across browsers can be challenging, especially with WebGL-based libraries that struggle with compatibility.
This guide walks through creating interactive graph visualizations using Vis.js and Next.js 16, replacing older WebGL-based approaches with a more robust HTML Canvas implementation that works everywhere.
Why Vis.js Over WebGL Libraries?
WebGL-based graph libraries like Sigma.js offer impressive performance but introduce several challenges:
| Feature | WebGL (Sigma.js) | HTML Canvas (Vis.js) |
|---|---|---|
| Browser Compatibility | Poor in Brave, Safari, some mobile browsers | Excellent across all browsers |
| Setup Complexity | Requires WebGL context handling | Simple API, no context management |
| Mobile Performance | Variable, often poor | Consistent performance |
| Accessibility | Limited | Better (canvas fallback) |
| Bundle Size | ~100KB (minified) | ~200KB (minified) |
| Learning Curve | Steep | Gentle |
For most applications, especially those targeting general audiences, Vis.js provides a better balance of performance, compatibility, and developer experience.
Project Setup
Start with a fresh Next.js 16 project:
npx create-next-app@latest graph-wizard
cd graph-wizard
npm install vis-network@10.1.0
Creating the Graph Component
Create a new component src/components/VisNetworkGraph.tsx:
'use client'
import { useEffect, useRef } from 'react'
import * as vis from 'vis-network'
interface GraphData {
nodes: vis.Node[]
edges: vis.Edge[]
}
interface VisNetworkGraphProps {
data: GraphData
options?: vis.Options
}
export default function VisNetworkGraph({ data, options }: VisNetworkGraphProps) {
const containerRef = useRef<HTMLDivElement>(null)
const networkRef = useRef<vis.Network | null>(null)
useEffect(() => {
if (!containerRef.current) return
// Create network
const network = new vis.Network(containerRef.current, data, options)
networkRef.current = network
// Cleanup on unmount
return () => {
network.destroy()
}
}, [data, options])
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
}
Example: Knowledge Graph Visualization
Here's a complete example showing a knowledge graph with AI-related entities:
'use client'
import { useState, useEffect } from 'react'
import * as vis from 'vis-network'
interface KnowledgeGraphProps {
data: {
nodes: Array<{ id: string; label: string; group: string; title?: string }>
edges: Array<{ from: string; to: string; label?: string; arrows?: string }>
}
}
export default function KnowledgeGraph({ data }: KnowledgeGraphProps) {
const containerRef = useRef<HTMLDivElement>(null)
const networkRef = useRef<vis.Network | null>(null)
// Configure the network
const options: vis.Options = {
nodes: {
shape: 'dot',
size: 25,
font: {
size: 16,
color: '#ffffff'
},
borderWidth: 2,
shadow: true
},
edges: {
width: 2,
color: {
color: '#999999',
highlight: '#ffffff',
hover: '#ffffff'
},
arrows: {
to: { enabled: true, scaleFactor: 0.5 }
},
smooth: {
type: 'continuous'
}
},
physics: {
stabilization: true,
barnesHut: {
gravitationalConstant: -2000,
springConstant: 0.04,
springLength: 100
}
},
interaction: {
hover: true,
tooltipDelay: 200
}
}
useEffect(() => {
if (!containerRef.current) return
const network = new vis.Network(containerRef.current, data, options)
networkRef.current = network
return () => {
network.destroy()
}
}, [data, options])
return (
<div style={{ width: '100%', height: '600px', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
<div ref={containerRef} />
</div>
)
}
// Example data
const knowledgeGraphData = {
nodes: [
{ id: '1', label: 'AI Agents', group: 'ai', title: 'Autonomous AI systems' },
{ id: '2', label: 'LLMs', group: 'ai', title: 'Large Language Models' },
{ id: '3', label: 'RAG', group: 'ai', title: 'Retrieval-Augmented Generation' },
{ id: '4', label: 'Vector DB', group: 'database', title: 'Vector Database' },
{ id: '5', label: 'Embeddings', group: 'ai', title: 'Vector Embeddings' },
{ id: '6', label: 'GraphRAG', group: 'ai', title: 'Graph-based RAG' },
{ id: '7', label: 'Knowledge Graph', group: 'graph', title: 'Knowledge Graph' },
{ id: '8', label: 'Neo4j', group: 'database', title: 'Neo4j Graph Database' },
{ id: '9', label: 'LangChain', group: 'framework', title: 'LangChain Framework' },
{ id: '10', label: 'AutoGen', group: 'framework', title: 'AutoGen Framework' }
],
edges: [
{ from: '1', to: '2', label: 'uses' },
{ from: '1', to: '3', label: 'uses' },
{ from: '3', to: '4', label: 'retrieves from' },
{ from: '3', to: '5', label: 'generates' },
{ from: '3', to: '7', label: 'enhances with' },
{ from: '7', to: '8', label: 'stored in' },
{ from: '1', to: '9', label: 'implemented with' },
{ from: '1', to: '10', label: 'implemented with' },
{ from: '3', to: '6', label: 'evolves into' }
]
}
export default function KnowledgeGraphExample() {
return <KnowledgeGraph data={knowledgeGraphData} />
}
Interactive Features
Vis.js provides built-in interactivity that enhances user experience:
const options: vis.Options = {
interaction: {
hover: true, // Highlight nodes on hover
tooltipDelay: 200, // Show tooltips with delay
zoomView: true, // Enable zoom
dragView: true, // Enable panning
selectConnectedEdges: true, // Highlight connected edges when selecting
multiselect: true, // Allow multiple selections
navigationButtons: true, // Show navigation buttons
keyboard: { // Keyboard navigation
enabled: true,
bindToWindow: true
}
}
}
Handling Events
You can listen to various events to create interactive applications:
const events = {
select: (params: { nodes: string[]; edges: string[] }) => {
console.log('Selected nodes:', params.nodes)
console.log('Selected edges:', params.edges)
},
hoverNode: (params: { node: string }) => {
console.log('Hovering over node:', params.node)
},
click: (params: { nodes: string[]; edges: string[] }) => {
console.log('Clicked on:', params.nodes)
console.log('Edges:', params.edges)
},
doubleClick: (params: { nodes: string[]; edges: string[] }) => {
console.log('Double-clicked on:', params.nodes)
},
oncontext: (params: { pointer: { x: number; y: number } }) => {
console.log('Right-click at:', params.pointer)
}
}
const network = new vis.Network(container, data, { ...options, events })
Performance Optimizations
For large graphs (1000+ nodes), implement these optimizations:
const options: vis.Options = {
// Limit physics simulation for large graphs
physics: {
stabilization: false,
barnesHut: {
gravitationalConstant: -2000,
springConstant: 0.04,
springLength: 100,
avoidOverlap: 0.5
}
},
// Enable lazy loading
nodes: {
shape: 'dot',
size: 20,
font: { size: 12 },
borderWidth: 1
},
// Limit initial zoom
interaction: {
zoomView: true,
dragView: true,
hover: true
},
// Enable canvas rendering for better performance
layout: {
improvedLayout: true,
hierarchicalRepulsion: true
}
}
Server-Side Rendering Considerations
For Next.js 16 Server Components, create a client wrapper:
'use client'
import { useEffect, useRef } from 'react'
import * as vis from 'vis-network'
interface GraphData {
nodes: vis.Node[]
edges: vis.Edge[]
}
interface VisNetworkGraphProps {
data: GraphData
options?: vis.Options
}
export default function VisNetworkGraph({ data, options }: VisNetworkGraphProps) {
const containerRef = useRef<HTMLDivElement>(null)
const networkRef = useRef<vis.Network | null>(null)
useEffect(() => {
if (!containerRef.current) return
const network = new vis.Network(containerRef.current, data, options)
networkRef.current = network
return () => {
network.destroy()
}
}, [data, options])
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
}
Best Practices
- Use TypeScript: Define clear interfaces for your graph data
- Handle Cleanup: Always destroy the network instance on unmount
- Optimize for Performance: Disable physics for static graphs
- Provide Feedback: Show tooltips and highlights on interaction
- Responsive Design: Use percentage-based heights and widths
- Accessibility: Consider keyboard navigation and screen readers
Conclusion
Vis.js provides a robust, cross-browser solution for interactive graph visualizations in Next.js 16 applications. By replacing WebGL-based libraries, you gain:
- Better Compatibility: Works in Brave, Safari, and all modern browsers
- Simpler Implementation: No WebGL context management required
- Consistent Performance: Reliable performance across devices
- Built-in Interactivity: Rich event handling and user interactions
For most applications, especially those targeting general audiences, Vis.js offers the best balance of features, performance, and developer experience. For specialised use cases requiring extreme performance with millions of nodes, consider WebGL approaches, but be prepared for compatibility challenges.