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

The entity model

Every object on the canvas, from a node with sockets to a sticky note or a video, is an entity with an id, a type, a position and data.

Everything you place on the canvas is an entity: a node with sockets, a frame, a sticky note, a line of text, a picture, a stroke of ink. You pass them all to KookieFlow in one entities array, and each one is a plain object with an id, a type, a position and data.

Every object is an entity

The canvas below holds a node, a frame with a node inside it, a text entity and a comment. All five objects sit in the same array, and only their type and data differ.

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';const initialEntities: Entity[] = [  {    id: 'load',    type: 'image/load',    position: { x: 0, y: 40 },    data: { label: 'Load image' },    outputs: [{ id: 'image', name: 'Image', type: 'image' }],  },  // The frame comes before the node inside it, so the node draws on top.  { id: 'stage', type: 'frame', position: { x: 300, y: 0 }, width: 300, height: 200, data: { label: 'Clean up' } },  {    id: 'blur',    type: 'image/blur',    parentId: 'stage',    position: { x: 330, y: 50 },    data: { label: 'Blur' },    inputs: [{ id: 'image', name: 'Image', type: 'image' }],    outputs: [{ id: 'result', name: 'Result', type: 'image' }],  },  {    id: 'title',    type: 'text',    position: { x: 660, y: 0 },    width: 240,    height: 40,    data: { content: 'Four kinds, one array', fontSize: 20 },  },  {    id: 'note',    type: 'comment',    position: { x: 660, y: 70 },    width: 220,    height: 110,    data: { content: 'A comment is a sticky note. It has no sockets.' },  },];export default function EntityKindsExample() {  const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities });  const flowRef = useRef<KookieFlowInstance>(null);  useEffect(() => {    flowRef.current?.fitView({ padding: 40, maxZoom: 1 });  }, []);  return (    <div style={{ width: '100%', height: 360 }}>      <KookieFlow        ref={flowRef}        entities={entities}        edges={edges}        onEntitiesChange={onEntitiesChange}        onEdgesChange={onEdgesChange}        onConnect={onConnect}      />    </div>  );}

Because every object has the same shape, you handle them all the same way. Selecting, copying, deleting and saving work on a comment exactly as they do on a node, and onEntitiesChange reports changes to both in the same form.

The fields every entity has

Entity is generic over its data: Entity<T extends EntityData = EntityData>. Four fields are required, and the rest are optional.

FieldTypeDefaultMeaning
idstringrequiredA unique id. Edges, parentId and every change event refer to the entity by it.
typestringrequiredWhich kind of entity this is. A built-in name draws that built-in; any other string draws a node.
positionXYPositionrequiredThe top-left corner, in world coordinates. y grows downward.
dataTrequiredThe entity's content. See Data.
widthnumberdepends on the typeThe width in world pixels. See Size and resizing.
heightnumberdepends on the typeThe height in world pixels.
inputsSocket[]noneInput sockets, drawn down the left edge. See Sockets and widgets.
outputsSocket[]noneOutput sockets, drawn down the right edge.
colorAccentColornoneA per-entity accent, drawn as a thin light along the top edge. See Entity styling.
resizableboolean | { width?: boolean; height?: boolean }trueWhether the resize handles appear, for both axes or for each one.
previewEntityPreviewnoneA band in the body that shows what an output socket holds. See Images, video and models.
parentIdstringnoneThe frame this entity belongs to. See Frames and groups.
collapsedbooleannoneWhether a frame is collapsed. Frames only.
selectedbooleannoneNot read by the canvas. See Read the selection from the canvas.
draggingbooleannoneNot read by the canvas.

position is always in world coordinates, including for an entity inside a frame. A child's position isn't an offset from its frame.

Data

EntityData has three named fields, and it accepts any other key you want to store:

FieldTypeMeaning
labelstringThe title in a node's header. A node with no label shows its type there instead.
statusEntityStatusA status drawn on the node's outline. See Evaluation.
statusMessagestringA message drawn under the node, in the error colour.

Other keys belong to you, except the ones a built-in type reads. The text and comment types read data.content, for example, and the chapter for each built-in type lists its fields. A node keeps its widget values in data.values, keyed by socket id, which the Sockets and widgets chapter covers.

Pass the data type as the generic argument when you want TypeScript to check it:

import type { Entity, EntityData } from '@kushagradhawan/kookie-flow';interface ShaderData extends EntityData {  source: string;  compiled: boolean;}const shader: Entity<ShaderData> = {  id: 'shader-1',  type: 'gl/shader',  position: { x: 120, y: 80 },  data: { label: 'Fragment shader', source: 'void main() {}', compiled: false },  inputs: [{ id: 'time', name: 'Time', type: 'float' }],  outputs: [{ id: 'color', name: 'Color', type: 'color' }],  color: 'violet',};

Read the selection from the canvas

selected and dragging are on the Entity type, but the canvas doesn't read or write either of them. It keeps the selection itself and doesn't report it through onEntitiesChange, so setting selected: true doesn't select an entity, and an entity someone selects doesn't gain selected: true. Call getSelectedEntities() on the flow's ref to read the selection, as the Selection chapter describes. toObject() leaves both fields out when you save.

Entities the canvas creates

Pressing T adds a text entity, and the pen adds one ink entity per stroke. Pasting a picture adds an image, unless you pass onFileDrop to handle it yourself. Each one arrives through onEntitiesChange as an add change, the same way an entity you add yourself would.

Nodes and self-drawn types

A node is any entity whose type isn't one of the seven self-drawn built-ins. The canvas gives a node a body, a header with its label, rows for its sockets and widgets, and a status outline. 'default', 'math/add' and 'image/blur' all draw as nodes, because none of them has a renderer of its own.

A self-drawn type has its own renderer, and the canvas doesn't put a node body or title on it. The seven self-drawn types are comment, reroute, text, image, video, mesh and draw.

A frame is the one built-in that isn't self-drawn. It uses the node body and header, and it holds other entities through their parentId.

To give a kind of node its own label, size and sockets once, instead of on every entity, use an entity types table.

The built-in types

BuiltInEntityType is the union of the eight built-in names.

typeWhat it isDataChapter
frameA container that holds other entities and can collapseFrameEntityDataFrames and groups
commentA sticky noteCommentEntityDataText and comments
textA block of text you can edit on the canvasTextEntityDataText and comments
imageA pictureImageEntityDataImages, video and models
videoA video clip with playback controlsVideoEntityDataImages, video and models
meshA 3D model you can turnMeshEntityDataImages, video and models
drawOne stroke of inkDrawEntityDataDrawing
rerouteA dot that an edge passes throughRerouteEntityDataEdges

Each built-in also has an entity type, such as TextEntity, and most have a type guard, such as isTextEntity. The Types chapter lists them.

Size and resizing

A node with no width is 240 pixels wide, which is DEFAULT_ENTITY_WIDTH. A node with no height is exactly as tall as its header, socket rows and preview band need. If you set a height larger than that, the socket rows are centred vertically in the extra space.

Give every entity that isn't a node an explicit width and height. Each self-drawn type has a drawing size of its own, such as 200 by 100 for a comment, but selection and hit testing fall back to the node size. Without both numbers, the area you can click can differ from what you see.

defaultEntityWidth on KookieFlow defaults to 240, and changing it doesn't make nodes wider. The node's body, selection outline and hit area stay 240 pixels wide, while its widget rows follow the prop, so any other value leaves the widgets out of line with the body. Leave the prop at its default. To make nodes wider, set width on the entity or defaultWidth on its type.

Resize an entity

Selecting an entity shows its resize handles. Drag a handle to resize, hold Shift to keep the proportions, and hold Alt to resize from the centre. Images, videos and models keep their proportions by default, so Shift frees them instead. When you release the handle, the canvas reports a dimensions change, plus a position change if the top or left edge moved.

resizable: false hides the handles, and resizable: { width: true, height: false } allows only horizontal resizing. When a data change sets a text entity's sizingMode, useGraph updates its resizable to match, as the Text and comments chapter explains.

A resize stops at a minimum size for each type:

TypeMinimum widthMinimum height
Node12040, or the height its header, sockets and preview band need if that is larger
frame10060
comment8040
text4020
image, video6040
mesh8080

Stacking order

When entities overlap, the canvas decides which one is on top. Entity has no z-index field.

  • Array order sets the starting order. An entity later in the array draws over one earlier in it. List a frame before the entities inside it.
  • Pressing an entity brings it to the top. A click, a drag or a press on a widget all count. Pressing a frame lifts everything inside it too, so a frame never covers its own children.
  • A new entity starts on top. This includes pasted entities and ones you add later.
  • A selected entity draws above every unselected one.
  • A comment is always on top. Comments are HTML laid over the canvas rather than drawn in it.

The canvas keeps the order separately from your entities and doesn't reorder your array, so the order isn't saved. A reloaded graph starts again from array order. To bring an entity to the top from code, call the store's bringToFront, which the store chapter covers.

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
  • Every object is an entity
  • The fields every entity has
  • Data
  • Read the selection from the canvas
  • Entities the canvas creates
  • Nodes and self-drawn types
  • The built-in types
  • Size and resizing
  • Resize an entity
  • Stacking order