React Flowchart — Build Interactive Flowcharts and Diagrams on Canvas with JavaScript
Build interactive flowcharts and connected diagrams directly on an HTML5 Canvas. Drag nodes around and watch the connecting arrows follow in real time — a common pattern for workflow builders, mind maps, org charts, and visual programming tools. Works with vanilla JavaScript, React (via react-konva), and Vue.
Instructions: Drag any node to reposition it. Connections update automatically. Click "Add Node" to add more.
- Vanilla
- React
- Vue
import Konva from 'konva'; var width = window.innerWidth; var height = window.innerHeight; var stage = new Konva.Stage({ container: 'container', width: width, height: height, }); var layer = new Konva.Layer(); stage.add(layer); var container = document.getElementById('container'); var controls = document.createElement('div'); controls.style.cssText = 'margin-bottom: 8px; display: flex; gap: 8px; align-items: center; font: 13px Arial, sans-serif;'; var addBtn = document.createElement('button'); addBtn.textContent = '+ Add Node'; addBtn.style.cssText = 'padding: 6px 14px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 600;'; addBtn.onclick = function() { var x = 100 + Math.random() * (width - 200); var y = 80 + Math.random() * (height - 160); var node = makeNode('New Node', '#2196F3', x, y); node.moveToTop(); layer.draw(); }; controls.appendChild(addBtn); var hint = document.createElement('span'); hint.textContent = 'Drag nodes to reposition. Arrows follow automatically.'; hint.style.cssText = 'color: #888; font-size: 12px;'; controls.appendChild(hint); container.parentNode.insertBefore(controls, container); var nodeList = []; var arrowList = []; function makeNode(label, color, x, y) { var group = new Konva.Group({ x: x, y: y, draggable: true }); var rect = new Konva.Rect({ width: 120, height: 50, fill: color, cornerRadius: 8, shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 6, shadowOffsetY: 2, offsetX: 60, offsetY: 25, }); var text = new Konva.Text({ text: label, fontSize: 14, fontFamily: 'Arial', fill: '#fff', width: 120, height: 50, align: 'center', verticalAlign: 'middle', offsetX: 60, offsetY: 25, }); group.add(rect); group.add(text); layer.add(group); var entry = { group: group }; nodeList.push(entry); group.on('dragmove', updateArrows); return group; } function connect(from, to) { var arrow = new Konva.Arrow({ points: [from.x(), from.y(), to.x(), to.y()], pointerLength: 10, pointerWidth: 8, fill: '#555', stroke: '#555', strokeWidth: 2, listening: false, }); layer.add(arrow); arrow.moveToBottom(); arrowList.push({ shape: arrow, from: from, to: to }); } function updateArrows() { for (var i = 0; i < arrowList.length; i++) { var a = arrowList[i]; a.shape.points([a.from.x(), a.from.y(), a.to.x(), a.to.y()]); } layer.batchDraw(); } var start = makeNode('Start', '#4CAF50', 100, 120); var procA = makeNode('Process A', '#2196F3', 300, 80); var decide = makeNode('Decision', '#FF9800', 500, 120); var procB = makeNode('Process B', '#2196F3', 300, 260); var end = makeNode('End', '#f44336', 500, 260); connect(start, procA); connect(procA, decide); connect(decide, procB); connect(decide, end); connect(procB, end); layer.draw();
import React, { useState, useCallback } from 'react'; import { Stage, Layer, Group, Rect, Text, Arrow } from 'react-konva'; var INITIAL_NODES = [ { id: 'start', label: 'Start', color: '#4CAF50', x: 100, y: 120 }, { id: 'procA', label: 'Process A', color: '#2196F3', x: 300, y: 80 }, { id: 'decide', label: 'Decision', color: '#FF9800', x: 500, y: 120 }, { id: 'procB', label: 'Process B', color: '#2196F3', x: 300, y: 260 }, { id: 'end', label: 'End', color: '#f44336', x: 500, y: 260 }, ]; var CONNECTIONS = [ { from: 'start', to: 'procA' }, { from: 'procA', to: 'decide' }, { from: 'decide', to: 'procB' }, { from: 'decide', to: 'end' }, { from: 'procB', to: 'end' }, ]; function FlowNode({ id, label, color, x, y, onDragMove }) { return ( <Group x={x} y={y} draggable onDragMove={function (e) { onDragMove(id, e.target.x(), e.target.y()); }} > <Rect width={120} height={50} fill={color} cornerRadius={8} shadowColor="rgba(0,0,0,0.15)" shadowBlur={6} shadowOffsetY={2} offsetX={60} offsetY={25} /> <Text text={label} fontSize={14} fontFamily="Arial" fill="#fff" width={120} height={50} align="center" verticalAlign="middle" offsetX={60} offsetY={25} /> </Group> ); } var App = function () { var [nodes, setNodes] = useState(INITIAL_NODES); var [connections, setConnections] = useState(CONNECTIONS); var [counter, setCounter] = useState(1); var handleDragMove = useCallback(function (id, x, y) { setNodes(function (prev) { return prev.map(function (n) { return n.id === id ? Object.assign({}, n, { x: x, y: y }) : n; }); }); }, []); var nodeMap = {}; nodes.forEach(function (n) { nodeMap[n.id] = n; }); var addNode = function () { var newId = 'new_' + counter; setCounter(counter + 1); setNodes(function (prev) { return prev.concat({ id: newId, label: 'New Node', color: '#2196F3', x: 100 + Math.random() * 400, y: 80 + Math.random() * 200, }); }); }; return ( <div> <div style={{ marginBottom: 8, display: 'flex', gap: 8, alignItems: 'center', font: '13px Arial, sans-serif' }}> <button onClick={addNode} style={{ padding: '6px 14px', background: '#2196F3', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer', fontSize: 13, fontWeight: 600 }} > + Add Node </button> <span style={{ color: '#888', fontSize: 12 }}>Drag nodes to reposition. Arrows follow automatically.</span> </div> <Stage width={window.innerWidth} height={window.innerHeight}> <Layer> {connections.map(function (c, i) { var fromNode = nodeMap[c.from]; var toNode = nodeMap[c.to]; if (!fromNode || !toNode) return null; return ( <Arrow key={'arrow-' + i} points={[fromNode.x, fromNode.y, toNode.x, toNode.y]} pointerLength={10} pointerWidth={8} fill="#555" stroke="#555" strokeWidth={2} listening={false} /> ); })} {nodes.map(function (n) { return ( <FlowNode key={n.id} id={n.id} label={n.label} color={n.color} x={n.x} y={n.y} onDragMove={handleDragMove} /> ); })} </Layer> </Stage> </div> ); }; export default App;
<template> <div> <div style="margin-bottom: 8px; display: flex; gap: 8px; align-items: center; font: 13px Arial, sans-serif;"> <button @click="addNode" style="padding: 6px 14px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 600;" > + Add Node </button> <span style="color: #888; font-size: 12px;">Drag nodes to reposition. Arrows follow automatically.</span> </div> <v-stage :config="stageConfig"> <v-layer> <v-arrow v-for="(c, i) in arrowConfigs" :key="'arrow-' + i" :config="c" /> <v-group v-for="n in nodes" :key="n.id" :config="{ x: n.x, y: n.y, draggable: true }" @dragmove="handleDragMove(n.id, $event)" > <v-rect :config="{ width: 120, height: 50, fill: n.color, cornerRadius: 8, shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 6, shadowOffsetY: 2, offsetX: 60, offsetY: 25, }" /> <v-text :config="{ text: n.label, fontSize: 14, fontFamily: 'Arial', fill: '#fff', width: 120, height: 50, align: 'center', verticalAlign: 'middle', offsetX: 60, offsetY: 25, }" /> </v-group> </v-layer> </v-stage> </div> </template> <script> import { ref, computed } from 'vue'; export default { setup() { var stageConfig = { width: window.innerWidth, height: window.innerHeight, }; var counter = ref(1); var nodes = ref([ { id: 'start', label: 'Start', color: '#4CAF50', x: 100, y: 120 }, { id: 'procA', label: 'Process A', color: '#2196F3', x: 300, y: 80 }, { id: 'decide', label: 'Decision', color: '#FF9800', x: 500, y: 120 }, { id: 'procB', label: 'Process B', color: '#2196F3', x: 300, y: 260 }, { id: 'end', label: 'End', color: '#f44336', x: 500, y: 260 }, ]); var connections = ref([ { from: 'start', to: 'procA' }, { from: 'procA', to: 'decide' }, { from: 'decide', to: 'procB' }, { from: 'decide', to: 'end' }, { from: 'procB', to: 'end' }, ]); var arrowConfigs = computed(function () { var nodeMap = {}; nodes.value.forEach(function (n) { nodeMap[n.id] = n; }); return connections.value.map(function (c) { var fromNode = nodeMap[c.from]; var toNode = nodeMap[c.to]; if (!fromNode || !toNode) return null; return { points: [fromNode.x, fromNode.y, toNode.x, toNode.y], pointerLength: 10, pointerWidth: 8, fill: '#555', stroke: '#555', strokeWidth: 2, listening: false, }; }).filter(Boolean); }); function handleDragMove(id, e) { var target = e.target; var list = nodes.value; for (var i = 0; i < list.length; i++) { if (list[i].id === id) { list[i].x = target.x(); list[i].y = target.y(); break; } } } function addNode() { var newId = 'new_' + counter.value; counter.value = counter.value + 1; nodes.value.push({ id: newId, label: 'New Node', color: '#2196F3', x: 100 + Math.random() * 400, y: 80 + Math.random() * 200, }); } return { stageConfig: stageConfig, nodes: nodes, connections: connections, arrowConfigs: arrowConfigs, handleDragMove: handleDragMove, addNode: addNode, }; }, }; </script>