A pure TypeScript visual node connector for creating draggable connections between nodes. Framework-agnostic and easy to use.
Watch the demo video to see power-link in action! Download video
- ๐ฏ Visual Node Connections - Create beautiful bezier curve connections between nodes
- ๐ฑ๏ธ Drag & Drop - Intuitive drag-and-drop connection creation
- ๐ Node Dragging - Move nodes around with automatic connection updates
- ๐งฒ Smart Snapping - Automatic connection point detection and snapping
- ๐จ Customizable - Fully configurable colors, sizes, and behaviors
- ๐ซ Delete Connections - Hover over connections to show delete button
- ๐ฆ Zero Dependencies - Pure JavaScript, no framework required
- ๐ญ Multiple Connection Points - Support for left and right connection dots
- ๐ Event Callbacks - Listen to connection and disconnection events
npm install power-linkOr using yarn:
yarn add power-linkOr using pnpm:
pnpm add power-linkimport Connector from "power-link";
// 1. Get container element
const container = document.getElementById("connector-container");
// 2. Create connector instance
const connector = new Connector({
container: container,
// Optional configuration
lineColor: "#155BD4",
lineWidth: 2,
dotSize: 12,
dotColor: "#155BD4",
// Event callbacks
onConnect: (connection) => {
console.log("Connection created:", connection);
// connection: { from: 'node1', to: 'node2', fromDot: 'right', toDot: 'left' }
},
onDisconnect: (connection) => {
console.log("Connection removed:", connection);
},
onViewChange: (viewState) => {
console.log("View changed:", viewState);
// viewState: { scale: 1, translateX: 0, translateY: 0 }
// Save view state to restore later
}
});
// 3. Register nodes
const node1 = document.getElementById("node1");
const node2 = document.getElementById("node2");
connector.registerNode("node1", node1, {
dotPositions: ["right"] // Only right connection dot
});
connector.registerNode("node2", node2, {
dotPositions: ["left", "right"] // Both left and right dots
});<div
id="connector-container"
style="position: relative; height: 600px;"
>
<div
id="node1"
style="position: absolute; left: 100px; top: 100px;"
>
Node 1
</div>
<div
id="node2"
style="position: absolute; left: 400px; top: 100px;"
>
Node 2
</div>
</div>| Option | Type | Default | Description |
|---|---|---|---|
container |
HTMLElement | Required | Container element for the connector |
lineColor |
String | '#155BD4' |
Color of connection lines |
lineWidth |
Number | 2 |
Width of connection lines |
dotSize |
Number | 12 |
Size of connection dots |
dotColor |
String | '#155BD4' |
Color of connection dots |
dotHoverScale |
Number | 1.8 |
Scale factor when hovering over connection dots |
deleteButtonSize |
Number | 20 |
Size of delete button |
enableNodeDrag |
Boolean | true |
Enable node dragging |
enableSnap |
Boolean | true |
Enable connection snapping |
snapDistance |
Number | 20 |
Snap distance in pixels |
enableZoom |
Boolean | true |
Enable zoom functionality |
enablePan |
Boolean | true |
Enable pan functionality |
minZoom |
Number | 0.1 |
Minimum zoom level (10%) |
maxZoom |
Number | 4 |
Maximum zoom level (400%) |
zoomStep |
Number | 0.1 |
Zoom step size (10%) |
onConnect |
Function | () => {} |
Callback when connection is created |
onDisconnect |
Function | () => {} |
Callback when connection is removed |
onViewChange |
Function | () => {} |
Callback when view state changes (zoom/pan) |
Register a node for connection.
Parameters:
id(String): Unique identifier for the nodeelement(HTMLElement): DOM element of the nodeoptions(Object): Node configurationdotPositions(String | Array): Connection dot positions'both': Both left and right dots['left', 'right']: Array format, both sides['left']: Only left dot['right']: Only right dot
info(Object): Node extraneous information
Returns: Node object
Example:
connector.registerNode("myNode", element, {
dotPositions: ["right"],
info: {
id: "123",
name: "apple",
desc: "this is a red apple"
}
});Programmatically create a connection between nodes.
Parameters:
fromNodeId(String): Source node IDtoNodeId(String): Target node IDfromDot(String): Source connection dot position -'left'or'right'(optional)toDot(String): Target connection dot position -'left'or'right'(optional)options(Object): Configuration options (optional)silent(boolean): Whether to create silently (without triggering callbacks)
Returns: Connection object or undefined
Example:
// Create connection with callbacks
connector.createConnection("node1", "node2");
// Create connection with specific dot positions
connector.createConnection("node1", "node2", "right", "left");
// Create connection silently without triggering callbacks
connector.createConnection("node1", "node2", "right", "left", { silent: true });Remove a connection.
Parameters:
connectionId(String): Connection ID (optional, if not provided, removes all connections)options(Object): Configuration options (optional)silent(boolean): Whether to disconnect silently (without triggering callbacks)
Example:
connector.disconnect(); // Remove all connections
connector.disconnect("connection-id"); // Remove specific connection
connector.disconnect("connection-id", { silent: true }); // Remove connection silently without triggering callbacksGet all connections.
Returns: Array of connection information
Example:
const connections = connector.getConnections();
// [{ id: '...', from: 'node1', to: 'node2', fromDot: 'right', toDot: 'left' }]Get all connections for a specific node.
Parameters:
nodeId(String): Node ID
Returns: Array of connection information
Update node position (called when node is moved).
Parameters:
nodeId(String): Node ID
Destroy the connector and clean up all resources.
Parameters:
options(Object): Configuration options (optional)silent(boolean): Whether to destroy silently (without triggering callbacks)
Example:
connector.destroy(); // Destroy silently by default (without triggering callbacks)
connector.destroy({ silent: false }); // Destroy non-silently (triggering callbacks)Set the view state (for initialization or restoring view).
Parameters:
state(Object): View state objectscale(Number): View scale (optional)translateX(Number): X-axis translation (optional)translateY(Number): Y-axis translation (optional)
Example:
connector.setViewState({
scale: 0.8,
translateX: 50,
translateY: 30
});Get the current view state.
Returns: ViewState object with scale, translateX, and translateY properties
Example:
const viewState = connector.getViewState();
console.log(viewState); // { scale: 1, translateX: 0, translateY: 0 }Set the zoom level (centered on canvas).
Parameters:
scale(Number): Zoom scale (will be clamped to minZoom and maxZoom)
Example:
connector.setZoom(1.5); // Zoom to 150%Get the current zoom level.
Returns: Number - Current zoom scale
Example:
const currentZoom = connector.getZoom();
console.log(currentZoom); // 1.0Zoom in by one step.
Example:
connector.zoomIn(); // Increase zoom by zoomStepZoom out by one step.
Example:
connector.zoomOut(); // Decrease zoom by zoomStepReset the view to default state (scale: 1, translateX: 0, translateY: 0).
Example:
connector.resetView(); // Reset to default viewUpdate all connection line positions (useful when container size changes or after manual node position updates).
Example:
connector.updateAllConnections(); // Refresh all connection lines<template>
<div
class="container"
ref="containerRef"
>
<div
class="node"
ref="node1Ref"
>
Node 1
</div>
<div
class="node"
ref="node2Ref"
>
Node 2
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import Connector from "power-link";
const containerRef = ref(null);
const node1Ref = ref(null);
const node2Ref = ref(null);
let connector = null;
onMounted(() => {
connector = new Connector({
container: containerRef.value,
onConnect: (connection) => {
console.log("Connection created:", connection);
},
onDisconnect: (connection) => {
console.log("Connection removed:", connection);
}
});
connector.registerNode("node1", node1Ref.value, {
dotPositions: ["right"],
info: {
id: "123",
name: "apple",
desc: "this is a red apple"
}
});
connector.registerNode("node2", node2Ref.value, {
dotPositions: ["left"],
info: {
id: "456",
name: "pear",
desc: "this is a yellow pear"
}
});
});
onBeforeUnmount(() => {
if (connector) {
connector.destroy();
}
});
</script>
<style scoped>
.container {
position: relative;
height: 600px;
background: #f5f5f5;
}
.node {
position: absolute;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: move;
}
</style>import { useEffect, useRef } from "react";
import Connector from "power-link";
function App() {
const containerRef = useRef(null);
const node1Ref = useRef(null);
const node2Ref = useRef(null);
const connectorRef = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
connectorRef.current = new Connector({
container: containerRef.current,
onConnect: (connection) => {
console.log("Connection created:", connection);
},
onDisconnect: (connection) => {
console.log("Connection removed:", connection);
}
});
connectorRef.current.registerNode("node1", node1Ref.current, {
dotPositions: ["right"],
info: {
id: "123",
name: "apple",
desc: "this is a red apple"
}
});
connectorRef.current.registerNode("node2", node2Ref.current, {
dotPositions: ["left"],
info: {
id: "456",
name: "pear",
desc: "this is a yellow pear"
}
});
return () => {
if (connectorRef.current) {
connectorRef.current.destroy();
}
};
}, []);
return (
<div
ref={containerRef}
style={{ position: "relative", height: "600px" }}
>
<div
ref={node1Ref}
style={{ position: "absolute", left: "100px", top: "100px" }}
>
Node 1
</div>
<div
ref={node2Ref}
style={{ position: "absolute", left: "400px", top: "100px" }}
>
Node 2
</div>
</div>
);
}<!DOCTYPE html>
<html>
<head>
<style>
#container {
position: relative;
height: 600px;
background: #f5f5f5;
}
.node {
position: absolute;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: move;
}
</style>
</head>
<body>
<div id="container">
<div
id="node1"
class="node"
style="left: 100px; top: 100px;"
>
Node 1
</div>
<div
id="node2"
class="node"
style="left: 400px; top: 100px;"
>
Node 2
</div>
</div>
<script type="module">
import Connector from "power-link";
const connector = new Connector({
container: document.getElementById("container"),
onConnect: (connection) => {
console.log("Connection created:", connection);
}
});
connector.registerNode("node1", document.getElementById("node1"), {
dotPositions: ["right"],
info: {
id: "123",
name: "apple",
desc: "this is a red apple"
}
});
connector.registerNode("node2", document.getElementById("node2"), {
dotPositions: ["left"],
info: {
id: "456",
name: "pear",
desc: "this is a yellow pear"
}
});
</script>
</body>
</html>// Node with both left and right connection points
connector.registerNode("centerNode", element, {
dotPositions: ["left", "right"]
});
// Node with only left connection point
connector.registerNode("endNode", element, {
dotPositions: ["left"],
info: {
id: "456",
name: "pear",
desc: "this is a yellow pear"
}
});
// Node with only right connection point
connector.registerNode("startNode", element, {
dotPositions: ["right"]
});Sometimes you may want to perform operations without triggering callbacks, such as when initializing connections from saved data or bulk operations.
// Silent connection creation (won't trigger onConnect callback)
connector.createConnection("node1", "node2", "right", "left", { silent: true });
// Silent disconnection (won't trigger onDisconnect callback)
connector.disconnect("connection-id", { silent: true });
// Silent destroy (won't trigger callbacks, default behavior)
connector.destroy(); // Default is silent
connector.destroy({ silent: false }); // Non-silent destroy (triggers callbacks)
// Example: Restore connections from saved data without triggering callbacks
const savedConnections = [
{ from: "node1", to: "node2", fromDot: "right", toDot: "left" },
{ from: "node2", to: "node3", fromDot: "right", toDot: "left" }
];
savedConnections.forEach((conn) => {
connector.createConnection(
conn.from,
conn.to,
conn.fromDot,
conn.toDot,
{ silent: true }
);
});You can attach custom information to nodes using the info parameter, which will be available in connection callbacks.
// Register node with custom info
const items = [
{ id: "node1", name: "Apple", desc: "This is a red apple", type: "fruit" },
{ id: "node2", name: "Pear", desc: "This is a yellow pear", type: "fruit" }
];
items.forEach((item) => {
const nodeElement = document.getElementById(item.id);
connector.registerNode(item.id, nodeElement, {
dotPositions: ["left"],
info: item // Attach custom info to the node
});
});
// Access node info in connection callbacks
const connector = new Connector({
container: container,
onConnect: async (connection) => {
console.log("Connection created:", connection);
console.log("From node info:", connection.fromInfo);
// { id: "node1", name: "Apple", desc: "This is a red apple", type: "fruit" }
console.log("To node info:", connection.toInfo);
// { id: "node2", name: "Pear", desc: "This is a yellow pear", type: "fruit" }
// You can use the info for saving to database, validation, etc.
await saveConnection({
from: connection.from,
to: connection.to,
fromInfo: connection.fromInfo,
toInfo: connection.toInfo
});
},
onDisconnect: (connection) => {
console.log("Connection removed:", connection);
console.log("From node info:", connection.fromInfo);
console.log("To node info:", connection.toInfo);
}
});const connector = new Connector({
container: container,
lineColor: "#FF6B6B", // Red connections
lineWidth: 3, // Thicker lines
dotSize: 16, // Larger dots
dotColor: "#4ECDC4", // Teal dots
deleteButtonSize: 24 // Larger delete button
});const connector = new Connector({
container: container,
onConnect: (connection) => {
console.log("New connection:", connection);
// { from: 'node1', to: 'node2', fromDot: 'right', toDot: 'left' }
// Save to database, update state, etc.
saveConnection(connection);
},
onDisconnect: (connection) => {
console.log("Connection removed:", connection);
// Update database, state, etc.
removeConnection(connection);
},
onViewChange: (viewState) => {
console.log("View changed:", viewState);
// { scale: 1, translateX: 0, translateY: 0 }
// Save view state to restore later
saveViewState(viewState);
}
});// Get current view state
const viewState = connector.getViewState();
console.log(viewState); // { scale: 1, translateX: 0, translateY: 0 }
// Set view state (restore saved view)
connector.setViewState({
scale: 0.8,
translateX: 100,
translateY: 50
});
// Zoom controls
connector.setZoom(1.5); // Set zoom to 150%
connector.zoomIn(); // Zoom in by one step
connector.zoomOut(); // Zoom out by one step
const currentZoom = connector.getZoom(); // Get current zoom
// Reset view
connector.resetView(); // Reset to default (scale: 1, translateX: 0, translateY: 0)
// Update all connections (useful after manual node position changes)
connector.updateAllConnections();- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
MIT License
Contributions, issues and feature requests are welcome!
If you have any questions or need help, please open an issue on GitHub.
Give a โญ๏ธ if this project helped you!
Made with โค๏ธ by the power-link team
