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>| Prop | Type | Description |
|---|---|---|
flow | Flow? | Full serialized flow. Prefer this when loading JSON returned by flow.toJSON(). |
nodes | FlowNode[] | Array of node definitions |
edges | FlowEdge[] | Array of edge definitions |
nodeTypes | NodeTypes | Map of node type names to components |
callbacks | FlowCallbacks | Event callback handlers |
config | FlowConfig | Optional configuration |
node_statuses | Record<string, NodeStatus>? | Per-node execution status passed to custom node components |
class | string? | Additional CSS class |
background | Snippet? | Custom background snippet |
controls | Snippet? | Controls snippet (e.g., zoom controls) |
children | Snippet? | 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>| Method | Type | Description |
|---|---|---|
getFlow() | () => FlowState | Returns the internal flow state for adding nodes, edges, viewport changes, and serialization. |
clientToCanvas() | (x: number, y: number) => Position | null | Converts browser client coordinates to canvas coordinates, including current pan and zoom. |
getViewport() | () => Viewport | Returns the current viewport values. |
getContainer() | () => HTMLDivElement | null | Returns 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" />
| Prop | Type | Description |
|---|---|---|
id | string | Unique ID within the node |
type | 'input' | 'output' | Handle direction |
port | string | Port type for connection matching |
position | 'left' | 'right' | 'top' | 'bottom' | Position on the node |
label | string? | Optional label text |
accept | string[]? | 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>| Prop | Type | Description |
|---|---|---|
width | number | Width in pixels (default: 200) |
height | number | Height in pixels (default: 150) |
backgroundColor | string? | Background color override |
nodeColor | string? | Node rectangle color override |
selectedNodeColor | string? | Selected node rectangle color override |
viewportColor | string? | Viewport indicator color override |
class | string? | 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>| Prop | Type | Description |
|---|---|---|
position | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | Position on canvas (default: 'bottom-left') |
showZoomLevel | boolean | Show zoom percentage (default: true) |
showFitView | boolean | Show fit view button (default: true) |
showLock | boolean | Show lock toggle button (default: true) |
class | string? | 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
| Field | Type | Description |
|---|---|---|
id | string | Stable node identifier. Edges reference this value. |
type | string | Looks up the renderer in nodeTypes, for example process or group. |
position | Position | Canvas coordinates. If parent_id is set, the position is relative to that parent group. |
data | Record<string, unknown> | Custom serializable payload passed to the node component as data. |
width / height | number? | Optional fixed dimensions. Useful for groups, sticky notes, and resizable nodes. |
parent_id | string? | Places a node inside a group node. Invalid or cyclic parent references are removed during load. |
z_index | number? | Optional drawing order override. |
Edge JSON
| Field | Type | Description |
|---|---|---|
source / target | string | Node IDs connected by the edge. Edges with missing nodes are ignored during load. |
source_handle / target_handle | string | Handle IDs on the source and target nodes. |
type | EdgeType? | Built-in path type or a custom key registered in edgeTypes. |
label | string? | Optional rendered label. |
style / animated / color | EdgeStyle? / boolean? / string? | Visual styling for built-in edge rendering. |
waypoints | Position[]? | Intermediate routing points for manually shaped edges. |
data | Record<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.dataandedge.data. - Custom node and edge type names must still be registered in
nodeTypesandedgeTypeswhen 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
flowprop for initial load orflow.fromJSON(json)after mount to restore the portal UI from the fullFlow. - Multiple
VirtualWireOutputNodeinstances with the samepair_idcreate fan-out: one input channel serializes to one direct edge per output target. - Passing only
json.nodesandjson.edgesto<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 unknownedge.datakeys.
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): voidSelection
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[] }): voidImport/Export
const json: Flow = flow.toJSON() flow.fromJSON(json: Flow): void