Getting started
InstallationQuick startControlled and uncontrolled state
Entities
The entity modelSockets and widgetsEntity typesFrames and groupsText and commentsImages, video and models
Edges and connections
EdgesConnections
Data
EvaluationSaving and exportingUndo and redoLayout and alignmentThe flow storeGraph queriesClipboard and cloning
Interaction
Canvas and cameraSelectionKeyboardSelection toolbarContext menusDrawingDropping and pasting media
Styling
ThemingEntity stylingFonts and text renderingMinimap
API reference
KookieFlowThe ref instanceHooksUtilitiesTypes
StartEntitiesDataAPI
MarkdownOpenAIClaude

Connections

Let people drag wires between sockets, turn a finished drag into an edge, and reject the connections your graph can't use.

People connect two entities by dragging a wire from one socket to another. When they let go over a socket that accepts the wire, the canvas calls onConnect, and your code adds the edge. You set which wires are allowed with connectionMode, isValidConnection and allowCycles.

Drag a wire between sockets

Press on any socket and drag. A wire can start from an output or from an input, and it follows the pointer until you let go.

While you drag, the canvas shows where the wire can go:

  • The wire has the colour of the socket type it started from.
  • Every socket on another entity that faces the other way and has a compatible type shows a halo in the success colour, --success-9.
  • When the socket under the pointer faces the same way, sits on the same entity or has an incompatible type, that socket and the wire turn the destructive colour, --destructive-9.
  • When allowCycles is false, the wire also turns the destructive colour over a socket that would close a loop.
  • Near the edge of the canvas, within 50 pixels, the view pans to follow the pointer, so you can reach an entity that is off screen.

Press Escape to drop the wire without connecting it.

These colours don't reflect connectionMode or your own isValidConnection. In the default loose mode, a wire that turned the destructive colour only because its types don't match still connects when you let go. A wire that your own rule rejects can look acceptable until you let go.

Create the edge

The canvas doesn't add an edge by itself. When a wire is accepted, it calls onConnect with a Connection, and you add the edge to the edges you pass back. Without an onConnect, the wire disappears when you let go.

FieldTypeMeaning
sourcestring | nullThe entity on the output side.
sourceSocketstring | nullThe output socket.
targetstring | nullThe entity on the input side.
targetSocketstring | nullThe input socket.
invalidbooleantrue when the two socket types aren't compatible.

source is always the output side, whichever end the drag started from. The canvas fills all four ids when it calls onConnect. They're typed as nullable, so check them before you use them as strings.

invalid reports type compatibility and nothing else. It is true for an incompatible wire that loose mode accepted, and also for one that your own isValidConnection accepted. An edge that keeps invalid: true draws as a dashed line in the destructive colour. The Edges chapter explains how to set it yourself.

useGraph returns an onConnect that appends an edge with the id `${source}-${sourceSocket}-${target}-${targetSocket}` and copies invalid onto it. It doesn't look for an edge that is already there. An input can take several wires, and dragging the same wire twice adds a second edge with the same id.

Write your own handler when an input should hold one wire. This one replaces whatever was plugged into the input, and reports the change through onEdgesChange so useGraph records it as one undo step when history is on:

editor.tsx
import { KookieFlow, useGraph, type Connection, type EdgeChange, type Entity } from '@kushagradhawan/kookie-flow';export function Editor({ initialEntities }: { initialEntities: Entity[] }) {  const { entities, edges, onEntitiesChange, onEdgesChange } = useGraph({ initialEntities });  function handleConnect({ source, sourceSocket, target, targetSocket, invalid }: Connection) {    if (!source || !sourceSocket || !target || !targetSocket) return;    // One wire per input: unplug whatever was in this one before.    const changes: EdgeChange[] = edges      .filter((edge) => edge.target === target && edge.targetSocket === targetSocket)      .map((edge): EdgeChange => ({ type: 'remove', id: edge.id }));    const id = `${source}.${sourceSocket}->${target}.${targetSocket}`;    changes.push({ type: 'add', edge: { id, source, sourceSocket, target, targetSocket, invalid } });    onEdgesChange(changes);  }  return (    <KookieFlow      entities={entities}      edges={edges}      onEntitiesChange={onEntitiesChange}      onEdgesChange={onEdgesChange}      onConnect={handleConnect}    />  );}

Choose loose or strict connections

connectionMode sets how strictly the canvas checks socket types when you let go of a wire.

  • 'loose' is the default. It accepts a wire between any output and any input on two different entities. An incompatible wire still connects, with invalid: true.
  • 'strict' also rejects a wire whose socket types aren't compatible.

Both modes reject a wire from an input to an input, from an output to an output, and from an entity to itself. Types are compatible when they're the same, when either is any, or when either type's compatibleWith lists the other or is '*'. The Sockets and widgets chapter covers those rules.

Arrow keys move between nodes. Press Enter to reach the focused node's controls, and Escape to come back here.

'use client';import { useEffect, useRef } from 'react';import { KookieFlow, useGraph, type Entity, type KookieFlowInstance } from '@kushagradhawan/kookie-flow';// A float output, an image output, and one node that takes one of each.const initialEntities: Entity[] = [  {    id: 'number',    type: 'default',    position: { x: 0, y: 0 },    data: { label: 'Number' },    outputs: [{ id: 'value', name: 'Value', type: 'float' }],  },  {    id: 'photo',    type: 'default',    position: { x: 0, y: 160 },    data: { label: 'Photo' },    outputs: [{ id: 'image', name: 'Image', type: 'image' }],  },  {    id: 'blur',    type: 'default',    position: { x: 400, y: 60 },    data: { label: 'Blur' },    inputs: [      { id: 'image', name: 'Image', type: 'image' },      { id: 'radius', name: 'Radius', type: 'float' },    ],  },];export default function ConnectionRulesExample() {  const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({    initialEntities,  });  const flowRef = useRef<KookieFlowInstance>(null);  useEffect(() => {    flowRef.current?.fitView({ padding: 60, maxZoom: 1 });  }, []);  return (    <div style={{ width: '100%', height: 360 }}>      {/* Strict: a wire only connects sockets whose types are compatible. */}      <KookieFlow        ref={flowRef}        entities={entities}        edges={edges}        onEntitiesChange={onEntitiesChange}        onEdgesChange={onEdgesChange}        onConnect={onConnect}        connectionMode="strict"      />    </div>  );}

This graph uses connectionMode="strict". Drag from Value to the Image input on Blur: the wire turns the destructive colour and disappears when you let go, because a float can't feed an image. Drag from Value to Radius instead, and the two float sockets connect.

PropTypeDefaultMeaning
onConnect(connection: Connection) => voidnoneCalled when a wire is accepted.
onConnectStart(event: PointerEvent, params: OnConnectStartParams) => voidnoneCalled when the pointer goes down on a socket.
onConnectEnd(event: PointerEvent, state: ConnectionEndState) => voidnoneCalled when a wire is let go, accepted or not.
connectionModeConnectionMode'loose''loose' or 'strict'.
isValidConnectionIsValidConnectionFnnoneYour own rule. Replaces the type check that connectionMode makes.
allowCyclesbooleantrueSet to false to reject a wire that closes a loop.

Write your own connection rule

Pass isValidConnection when the rule depends on more than socket types. The canvas calls it when you let go over a socket, after its own checks for direction, for two different entities and for sockets that exist. If it returns false, the canvas rejects the wire.

Your rule replaces connectionMode, so the canvas no longer checks types for you. Call areTypesCompatible inside the rule if you still want them checked. The socketTypes argument is your table merged over the built-in socket types.

This rule rejects a second wire into an input that is already connected:

editor.tsx
import {  areTypesCompatible,  KookieFlow,  useGraph,  type Entity,  type IsValidConnectionFn,} from '@kushagradhawan/kookie-flow';export function Editor({ initialEntities }: { initialEntities: Entity[] }) {  const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities });  const isValidConnection: IsValidConnectionFn = (params, socketTypes) => {    // The drag can start at either end, so find the input first.    const input = params.source.isInput ? params.source : params.target;    const taken = edges.some((edge) => edge.target === input.entityId && edge.targetSocket === input.socketId);    return !taken && areTypesCompatible(params.sourceSocketType, params.targetSocketType, socketTypes);  };  return (    <KookieFlow      entities={entities}      edges={edges}      onEntitiesChange={onEntitiesChange}      onEdgesChange={onEdgesChange}      onConnect={onConnect}      isValidConnection={isValidConnection}    />  );}

IsValidConnectionFn is (params: ConnectionValidationParams, socketTypes: Record<string, SocketType>) => boolean.

ConnectionValidationParams fieldTypeMeaning
sourceSocketHandleThe socket the drag started from. It can be an input.
targetSocketHandleThe socket under the pointer.
sourceSocketTypestringThe type of source.
targetSocketTypestringThe type of target.

Unlike a Connection, these params follow the drag rather than the data flow. When someone drags from an input to an output, params.source is the input.

A SocketHandle names one socket:

FieldTypeMeaning
entityIdstringThe entity the socket belongs to.
socketIdstringThe socket's id.
isInputbooleantrue for an input, false for an output.

Reject cycles

allowCycles defaults to true, so a wire can close a loop. Set it to false when your graph has to run in order, such as a processing pipeline, where a loop has no first step.

<KookieFlow entities={entities} edges={edges} onConnect={onConnect} allowCycles={false} />

With allowCycles={false}, the canvas rejects a wire when its target entity already leads back to its source entity through existing edges. The wire turns the destructive colour as soon as it reaches such a socket. The loop check runs after connectionMode or your own isValidConnection, so it applies with either one. To ask the same question in your own code, use wouldCreateGraphCycle from Graph queries.

Follow a drag from start to end

onConnectStart runs as soon as the pointer goes down on a socket, before it moves. onConnectEnd runs when you let go, after onConnect if the wire was accepted.

OnConnectStartParams fieldTypeMeaning
entityIdstringThe entity the drag started on.
socketIdstringThe socket the drag started on.
isInputbooleanWhether that socket is an input.
ConnectionEndState fieldTypeMeaning
isValidbooleantrue when the wire was accepted and onConnect was called.
source{ entityId: string; socketId: string; isInput: boolean }The socket the drag started on.
positionXYPositionWhere the pointer was let go, in world coordinates, the space entity positions use.

onConnectEnd isn't called when Escape cancels the drag. isValid is false both when the wire is let go over empty canvas and when it is let go over a socket that rejects it.

A common use is to create a node where a wire is dropped, already connected to the socket the drag started from:

editor.tsx
import { KookieFlow, useGraph, type ConnectionEndState, type Entity } from '@kushagradhawan/kookie-flow';export function Editor({ initialEntities }: { initialEntities: Entity[] }) {  const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities });  function handleConnectEnd(_event: PointerEvent, { isValid, source, position }: ConnectionEndState) {    if (isValid) return;    const id = `node-${Date.now()}`;    const socket = { id: 'value', name: 'Value', type: 'any' };    // A drag from an input needs a node with an output, placed to the left of the drop point.    const fromInput = source.isInput;    onEntitiesChange([{      type: 'add',      entity: fromInput        ? { id, type: 'default', position: { x: position.x - 200, y: position.y }, width: 200, data: { label: 'New node' }, outputs: [socket] }        : { id, type: 'default', position, width: 200, data: { label: 'New node' }, inputs: [socket] },    }]);    onEdgesChange([{      type: 'add',      edge: fromInput        ? { id: `${id}-wire`, source: id, sourceSocket: 'value', target: source.entityId, targetSocket: source.socketId }        : { id: `${id}-wire`, source: source.entityId, sourceSocket: source.socketId, target: id, targetSocket: 'value' },    }]);  }  return (    <KookieFlow      entities={entities}      edges={edges}      onEntitiesChange={onEntitiesChange}      onEdgesChange={onEdgesChange}      onConnect={onConnect}      onConnectEnd={handleConnectEnd}    />  );}

Because isValid is also false for a rejected wire, this handler also adds a node when a wire is let go over a socket that rejects it.

Check compatibility in your own code

The canvas exports the checks it uses, so a menu, a shortcut or an import step can apply the same rules.

FunctionReturns true when
areTypesCompatible(typeA, typeB, socketTypes)The two types are compatible. The order of the types doesn't matter.
isSocketCompatible(source, target, entitiesOrMap, socketTypes)The two sockets face opposite ways, belong to different entities, both exist, and have compatible types. This is the check behind the wire's colour during a drag, and behind an edge's invalid.
validateConnection(source, target, entitiesOrMap, socketTypes, connectionMode?, isValidConnection?)The canvas would accept the wire when you let go, apart from the loop check. connectionMode defaults to 'loose', and isValidConnection replaces the type check when you pass it.

source and target are SocketHandle objects. entitiesOrMap takes an Entity[] or a Map<string, Entity>. Pass a map when you check many sockets, because the function then looks each entity up directly instead of searching the array.

This checks a wire before a button adds it:

import { validateConnection, type Edge, type Entity, type SocketType } from '@kushagradhawan/kookie-flow';export function connectIfAllowed(  entities: Entity[],  edges: Edge[],  socketTypes: Record<string, SocketType>): Edge[] {  const output = { entityId: 'noise', socketId: 'image', isInput: false };  const input = { entityId: 'blur', socketId: 'image', isInput: true };  if (!validateConnection(output, input, entities, socketTypes, 'strict')) return edges;  return [...edges, { id: 'noise-to-blur', source: 'noise', sourceSocket: 'image', target: 'blur', targetSocket: 'image' }];}
Getting started
  • Installation
  • Quick start
  • Controlled and uncontrolled state
Entities
  • The entity model
  • Sockets and widgets
  • Entity types
  • Frames and groups
  • Text and comments
  • Images, video and models
Edges and connections
  • Edges
  • Connections
Data
  • Evaluation
  • Saving and exporting
  • Undo and redo
  • Layout and alignment
  • The flow store
  • Graph queries
  • Clipboard and cloning
Interaction
  • Canvas and camera
  • Selection
  • Keyboard
  • Selection toolbar
  • Context menus
  • Drawing
  • Dropping and pasting media
Styling
  • Theming
  • Entity styling
  • Fonts and text rendering
  • Minimap
API reference
  • KookieFlow
  • The ref instance
  • Hooks
  • Utilities
  • Types
Reference
  • Live demo
  • llms.txt
  • GitHub
MIT licensed. © 2026 Kushagra Dhawan.
On this page
  • Drag a wire between sockets
  • Create the edge
  • Choose loose or strict connections
  • Write your own connection rule
  • Reject cycles
  • Follow a drag from start to end
  • Check compatibility in your own code