API Reference

Complete reference for kaykay components, types, and configuration.

Components

Canvas

The main component that renders the flow diagram.

<Canvas
  flow={savedFlow}
  nodeTypes={nodeTypes}
  callbacks={callbacks}
  config={config}
  node_statuses={nodeStatuses}
>
  {#snippet background()}
    <!-- Custom background -->
  {/snippet}
  {#snippet controls()}
    <Controls />
  {/snippet}
  {#snippet children()}
    <Minimap />
  {/snippet}
</Canvas>
PropTypeDescription
flowFlow?Full serialized flow. Prefer this when loading JSON returned by flow.toJSON().
nodesFlowNode[]Array of node definitions
edgesFlowEdge[]Array of edge definitions
nodeTypesNodeTypesMap of node type names to components
callbacksFlowCallbacksEvent callback handlers
configFlowConfigOptional configuration
node_statusesRecord<string, NodeStatus>?Per-node execution status passed to custom node components
classstring?Additional CSS class
backgroundSnippet?Custom background snippet
controlsSnippet?Controls snippet (e.g., zoom controls)
childrenSnippet?Additional children (e.g., Minimap)

Canvas Instance Methods

Bind the Canvas component when you need imperative access, such as adding a node where a palette item is dropped.

let canvasRef: ReturnType<typeof Canvas> | undefined;

function handleDrop(event: DragEvent) {
  event.preventDefault();

  const position = canvasRef?.clientToCanvas(
    event.clientX,
    event.clientY
  );
  if (!position) return;

  const node: FlowNode = {
    id: crypto.randomUUID(),
    type: 'custom',
    position,
    data: { label: 'Dropped node' }
  };

  canvasRef?.getFlow().addNode(node);
}

<div ondragover={(event) => event.preventDefault()} ondrop={handleDrop}>
  <Canvas bind:this={canvasRef} {nodes} {edges} {nodeTypes} />
</div>
MethodTypeDescription
getFlow()() => FlowStateReturns the internal flow state for adding nodes, edges, viewport changes, and serialization.
clientToCanvas()(x: number, y: number) => Position | nullConverts browser client coordinates to canvas coordinates, including current pan and zoom.
getViewport()() => ViewportReturns the current viewport values.
getContainer()() => HTMLDivElement | nullReturns the canvas container element for wiring native browser events.

Handle

Connection point on a node for edges.

<Handle
  id="input-1"
  type="input"
  port="data"
  position="left"
  label="Input"
/>
PropTypeDescription
idstringUnique ID within the node
type'input' | 'output'Handle direction
portstringPort type for connection matching
position'left' | 'right' | 'top' | 'bottom'Position on the node
labelstring?Optional label text
acceptstring[]?Additional port types to accept

HandleGroup

Groups multiple handles on one side of a node.

<HandleGroup position="left">
  <Handle id="in-1" type="input" port="data" />
  <Handle id="in-2" type="input" port="data" />
</HandleGroup>

GroupNode

Built-in component for creating visual node groups.

import { GroupNode } from 'kaykay';

const nodeTypes = {
  group: GroupNode,
  // ... other types
};

VirtualWireInputNode / VirtualWireOutputNode

Built-in portal nodes for reducing long edge clutter. Connect into a numbered input channel, then connect out of the matching output channel elsewhere on the canvas. Multiple output portals can share the same pair ID to fan one channel out to multiple targets. No long pair edge is drawn between the portal nodes. Users can rename the shared pair label, add channels from the header, rename labels inline, and delete unused channels while stable IDs remain in kaykay editor metadata.

import {
  VirtualWireInputNode,
  VirtualWireOutputNode,
  resolveVirtualWireEdges
} from 'kaykay';

const nodeTypes = {
  'virtual-wire-input': VirtualWireInputNode,
  'virtual-wire-output': VirtualWireOutputNode
};

const nodes = [
  {
    id: 'wire-in',
    type: 'virtual-wire-input',
    position: { x: 260, y: 120 },
    data: {
      pair_id: 'main-bus',
      pair_label: 'Main Bus',
      label: 'Main Bus In',
      channels: [{ id: '1', label: '1' }]
    }
  },
  {
    id: 'wire-out-a',
    type: 'virtual-wire-output',
    position: { x: 720, y: 120 },
    data: {
      pair_id: 'main-bus',
      pair_label: 'Main Bus',
      label: 'Main Bus Out',
      channels: [{ id: '1', label: '1' }]
    }
  },
  {
    id: 'wire-out-b',
    type: 'virtual-wire-output',
    position: { x: 720, y: 240 },
    data: {
      pair_id: 'main-bus',
      pair_label: 'Main Bus',
      label: 'Main Bus Mirror Out',
      channels: [{ id: '1', label: '1' }]
    }
  }
];

// For execution/dataflow, collapse portal segments into logical direct edges.
const logicalEdges = resolveVirtualWireEdges(nodes, edges);

Minimap

Optional minimap for navigation. Click, drag, touch, or keyboard arrows to pan viewport.

<Canvas {nodes} {edges} {nodeTypes}>
  {#snippet children()}
    <Minimap width={200} height={150} />
  {/snippet}
</Canvas>
PropTypeDescription
widthnumberWidth in pixels (default: 200)
heightnumberHeight in pixels (default: 150)
backgroundColorstring?Background color override
nodeColorstring?Node rectangle color override
selectedNodeColorstring?Selected node rectangle color override
viewportColorstring?Viewport indicator color override
classstring?Additional CSS class

Controls

Zoom controls with optional fit view and lock toggle.

<Canvas {nodes} {edges} {nodeTypes}>
  {#snippet controls()}
    <Controls position="bottom-left" />
  {/snippet}
</Canvas>
PropTypeDescription
position'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'Position on canvas (default: 'bottom-left')
showZoomLevelbooleanShow zoom percentage (default: true)
showFitViewbooleanShow fit view button (default: true)
showLockbooleanShow lock toggle button (default: true)
classstring?Additional CSS class

Types

FlowNode

interface FlowNode<T = Record<string, unknown>> {
  id: string;
  type: string;
  position: { x: number; y: number };
  data: T;
  width?: number;
  height?: number;
  selected?: boolean;
  parent_id?: string;  // For grouping
  z_index?: number;
}

FlowEdge

interface FlowEdge<T = Record<string, unknown>> {
  id: string;
  source: string;
  source_handle: string;
  target: string;
  target_handle: string;
  type?: 'bezier' | 'straight' | 'step' | string;
  style?: 'solid' | 'dashed' | 'dotted';
  color?: string;
  animated?: boolean;
  label?: string;
  waypoints?: Position[];
  data?: T;
}

VirtualWireNodeData

interface VirtualWireNodeData {
  pair_id: string;
  pair_label?: string;
  label?: string;
  color?: string;
  channels?: VirtualWireChannel[];
}

interface VirtualWireChannel {
  id: string;      // "1", "2", "3", ...
  label?: string;
  port?: string;
}

NodeProps

interface NodeProps<T = Record<string, unknown>> {
  id: string;
  data: T;
  selected: boolean;
  status?: NodeStatus;
}

FlowCallbacks

interface FlowCallbacks {
  on_node_click?: (node_id: string) => void;
  on_node_drag_start?: (node_id: string) => void;
  on_node_drag_end?: (node_id: string, position: Position) => void;
  on_connect?: (edge: FlowEdge) => void;
  on_edge_click?: (edge_id: string) => void;
  on_delete?: (node_ids: string[], edge_ids: string[]) => void;
  on_viewport_change?: (viewport: Viewport) => void;
  on_selection_change?: (node_ids: string[], edge_ids: string[]) => void;
  on_undo?: () => void;
  on_redo?: () => void;
  on_change?: (flow: Flow, reason: FlowChangeReason) => void;
}

FlowConfig

interface FlowConfig {
  min_zoom?: number;      // Default: 0.1
  max_zoom?: number;      // Default: 4
  snap_to_grid?: boolean; // Default: false
  grid_size?: number;     // Default: 10
  allow_delete?: boolean; // Default: true
  default_edge_type?: EdgeType;
  locked?: boolean;       // Prevent modifications
  max_history?: number;   // Default: 50
  max_connections_per_input?: number;
  max_connections_per_output?: number;
  prevent_cycles?: boolean;
  is_valid_connection?: (context: ConnectionValidationContext) => ConnectionValidationResult;
}

JSON Flow Model

The import/export format is plain JSON. Use it to persist a diagram, send it to an API, or power an editor like the Playground live JSON panel.

Flow

Flow is the complete serialized diagram returned by flow.toJSON() and accepted by flow.fromJSON().

interface Flow {
  nodes: FlowNode[];
  edges: FlowEdge[];
}

const json = canvasRef.getFlow().toJSON();
canvasRef.getFlow().fromJSON(json);

Example JSON

{
  "nodes": [
    {
      "id": "source",
      "type": "input",
      "position": { "x": 80, "y": 120 },
      "data": { "label": "Source" }
    },
    {
      "id": "group-a",
      "type": "group",
      "position": { "x": 40, "y": 60 },
      "width": 320,
      "height": 220,
      "data": { "label": "Processing" }
    },
    {
      "id": "transform",
      "type": "process",
      "position": { "x": 40, "y": 70 },
      "parent_id": "group-a",
      "data": { "operation": "Transform" }
    }
  ],
  "edges": [
    {
      "id": "source-transform",
      "source": "source",
      "source_handle": "out",
      "target": "transform",
      "target_handle": "in",
      "type": "bezier",
      "label": "raw data",
      "style": "dashed",
      "animated": true,
      "color": "#eb5425",
      "waypoints": [{ "x": 220, "y": 150 }],
      "data": { "schema": "raw-event" }
    }
  ]
}

Node JSON

FieldTypeDescription
idstringStable node identifier. Edges reference this value.
typestringLooks up the renderer in nodeTypes, for example process or group.
positionPositionCanvas coordinates. If parent_id is set, the position is relative to that parent group.
dataRecord<string, unknown>Custom serializable payload passed to the node component as data.
width / heightnumber?Optional fixed dimensions. Useful for groups, sticky notes, and resizable nodes.
parent_idstring?Places a node inside a group node. Invalid or cyclic parent references are removed during load.
z_indexnumber?Optional drawing order override.

Edge JSON

FieldTypeDescription
source / targetstringNode IDs connected by the edge. Edges with missing nodes are ignored during load.
source_handle / target_handlestringHandle IDs on the source and target nodes.
typeEdgeType?Built-in path type or a custom key registered in edgeTypes.
labelstring?Optional rendered label.
style / animated / colorEdgeStyle? / boolean? / string?Visual styling for built-in edge rendering.
waypointsPosition[]?Intermediate routing points for manually shaped edges.
dataRecord<string, unknown>?Custom serializable payload passed to custom edge components.

Handles and Ports

Handles are declared by node components, not stored on each node JSON object. Edges store the selected handle IDs, while port and accept on <Handle> control whether a new connection is valid.

<Handle id="out" type="output" port="text" position="right" />
<Handle id="in" type="input" port="processed" accept={["text"]} position="left" />

{
  "source": "source",
  "source_handle": "out",
  "target": "transform",
  "target_handle": "in"
}

Viewport JSON

Viewport is runtime pan/zoom state. It is available through viewport APIs, but it is not included in Flow serialization.

interface Viewport {
  x: number;
  y: number;
  zoom: number;
}

const viewport = canvasRef.getViewport();
canvasRef.getFlow().setViewport(viewport);

Import and Export Notes

  • toJSON() returns cloned node and edge data so callers can safely stringify or store it.
  • fromJSON() normalizes invalid nodes, duplicate IDs, dangling edges, and cyclic group parents before loading.
  • Only serializable values should be stored in node.data and edge.data.
  • Custom node and edge type names must still be registered in nodeTypes and edgeTypes when the JSON is loaded.

Virtual Wire Resolution

flow.toJSON() stores virtual wires as backend-compatible direct edges in nodes/edges. New kaykay UIs restore the portal nodes from kaykay.virtual_wires metadata during fromJSON(); older engines can ignore that key.

  • Use the flow prop for initial load or flow.fromJSON(json) after mount to restore the portal UI from the full Flow.
  • Multiple VirtualWireOutputNode instances with the same pair_id create fan-out: one input channel serializes to one direct edge per output target.
  • Passing only json.nodes and json.edges to <Canvas> intentionally renders the backend-compatible direct-edge fallback because top-level metadata is unavailable.
  • During fromJSON(), current direct edges are the source of truth. If an older tool deletes or retargets a flattened direct edge, kaykay mirrors that change instead of restoring stale portal connections.
  • Resolved direct edges store kaykay details at edge.data.kaykay_virtual_wire; engines that do not need editor details can ignore unknown edge.data keys.
const serialized = flow.toJSON();
// serialized.nodes has no virtual-wire nodes.
// serialized.edges has source -> target direct edges.
// serialized.kaykay.virtual_wires lets kaykay restore the editor portal view.

// source -> virtual input 1 + virtual output 1 -> target
// is saved as source -> target for dataflow/execution.

Flow State Methods

Access the flow state via canvas.getFlow():

Node Operations

flow.addNode(node: FlowNode): string
flow.removeNode(nodeId: string): void
flow.getNode(nodeId: string): NodeState | undefined
flow.updateNodePosition(nodeId: string, position: Position): void
flow.updateNodeData(nodeId: string, data: Partial<T>): void
flow.updateNodeDimensions(nodeId: string, width: number, height: number): void
flow.resizeNode(nodeId: string, width: number, height: number): void
flow.getChildNodes(parentId: string): NodeState[]
flow.getAbsolutePosition(nodeId: string): Position
flow.setNodeParent(nodeId: string, parentId: string | undefined): void
flow.updateGroupMembership(groupId: string): void

Edge Operations

flow.addEdge(edge: FlowEdge): boolean
flow.removeEdge(edgeId: string): void
flow.getEdge(edgeId: string): FlowEdge | undefined
flow.updateEdge(edgeId: string, updates: Partial<FlowEdge>): void
flow.canConnect(sourceNodeId: string, sourceHandleId: string, targetNodeId: string, targetHandleId: string): boolean
flow.getConnectionValidation(sourceNodeId: string, sourceHandleId: string, targetNodeId: string, targetHandleId: string): { valid: boolean; reason?: string }
flow.canConnectPorts(sourcePort: string, targetPort: string, targetAccept?: string[]): boolean

// Waypoints for custom edge routing
flow.addEdgeWaypoint(edgeId: string, position: Position, index?: number): void
flow.updateEdgeWaypoint(edgeId: string, waypointIndex: number, position: Position): void
flow.removeEdgeWaypoint(edgeId: string, waypointIndex: number): void
flow.clearEdgeWaypoints(edgeId: string): void

Selection

flow.selectNode(nodeId: string, additive?: boolean): void
flow.selectEdge(edgeId: string, additive?: boolean): void
flow.selectAll(): void
flow.clearSelection(): void
flow.deleteSelected(): void

// Read selected IDs
flow.selected_node_ids: Set<string>
flow.selected_edge_ids: Set<string>

Viewport

flow.setViewport(viewport: Viewport): void
flow.pan(dx: number, dy: number): void
flow.zoom(delta: number, center: Position): void
flow.zoomIn(step?: number): void
flow.zoomOut(step?: number): void
flow.resetZoom(): void
flow.setZoom(zoom: number): void
flow.fitView(padding?: number): void
flow.screenToCanvas(screenPos: Position): Position
flow.canvasToScreen(canvasPos: Position): Position

// Read viewport state
flow.viewport: Viewport  // { x, y, zoom }

Lock State

flow.locked: boolean        // Read lock state
flow.setLocked(locked: boolean): void
flow.toggleLocked(): void

Handle Operations

flow.getHandle(nodeId: string, handleId: string): HandleState | undefined
flow.getHandlePosition(nodeId: string, handleId: string): Position | undefined

History and Clipboard

flow.undo(): boolean
flow.redo(): boolean
flow.canUndo: boolean
flow.canRedo: boolean
flow.clearHistory(): void
flow.beginTransaction(): void
flow.endTransaction(commit?: boolean, reason?: FlowChangeReason): void
flow.copySelected(): void
flow.paste(position?: Position, clipboardData?: { nodes: FlowNode[]; edges: FlowEdge[] }): void

Import/Export

const json: Flow = flow.toJSON()
flow.fromJSON(json: Flow): void