Sockets and widgets
Give an entity typed inputs and outputs. An unconnected input can show a control, such as a slider or a text field, that sets its value.
A socket is a typed connection point on an entity. Inputs sit on the left edge and outputs on the right, and an edge always runs from an output to an input. When nothing is wired into an input, the input can show a widget, such as a slider or a text field, so a person can set its value by hand.
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: 'value', type: 'math/value', position: { x: 0, y: 120 }, width: 160, data: { label: 'Value' }, outputs: [{ id: 'out', name: 'Value', type: 'float' }], }, { id: 'render', type: 'image/render', position: { x: 240, y: 0 }, width: 300, data: { label: 'Render', values: { steps: 30, mode: 'Cubic', live: true, fit: 'Fill', offset: [0, 1.5, -2], size: [1024, 768], seed: 42 }, }, inputs: [ { id: 'prompt', name: 'Prompt', type: 'string', layout: 'stacked', widget: 'textarea', rows: 3, placeholder: 'Describe the image', }, // Wire Value into Strength and its slider goes away. { id: 'strength', name: 'Strength', type: 'float', defaultValue: 0.5 }, { id: 'steps', name: 'Steps', type: 'int' }, { id: 'mode', name: 'Mode', type: 'enum', options: ['Linear', 'Cubic'] }, { id: 'tiled', name: 'Tiled', type: 'boolean' }, { id: 'tint', name: 'Tint', type: 'color', defaultValue: '#3e63dd' }, { id: 'live', name: 'Live', type: 'boolean', widget: 'switch' }, { id: 'fit', name: 'Fit', type: 'enum', widget: 'segmented', options: ['Fit', 'Fill', 'Crop'] }, // Drag a component sideways to change it, or click it to type. { id: 'offset', name: 'Offset', type: 'vec3', step: 0.1 }, { id: 'size', name: 'Size', type: 'vec2', step: 1, min: 64, max: 4096 }, { id: 'seed', name: 'Seed', type: 'seed' }, ], outputs: [{ id: 'image', name: 'Image', type: 'image' }], },];export default function NodesExample() { 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: 640 }}> <KookieFlow ref={flowRef} entities={entities} edges={edges} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} /> </div> );}The Render node's inputs show ten of the eleven built-in widgets. Drag a wire from Value into Strength and the slider disappears, because the value now comes down the wire.
Add inputs and outputs
List sockets in an entity's inputs and outputs. Each socket needs an id, a name and a
type:
import type { Entity } from '@kushagradhawan/kookie-flow';const resize: Entity = { id: 'resize-1', type: 'image/resize', position: { x: 0, y: 0 }, data: { label: 'Resize' }, inputs: [ { id: 'image', name: 'Image', type: 'image' }, { id: 'width', name: 'Width', type: 'int', min: 1, max: 4096, defaultValue: 1024 }, { id: 'keepAspect', name: 'Keep aspect', type: 'boolean', defaultValue: true }, ], outputs: [{ id: 'resized', name: 'Image', type: 'image' }],};An edge names the sockets at its two ends by id, so keep ids stable once you save a graph. The
name is the label the canvas draws beside the socket.
Outputs are laid out first, from the top of the body, and inputs follow underneath them. A node
with no height grows to fit every row.
| Field | Type | Default | Meaning |
|---|---|---|---|
id | string | required | The socket's id. Edges refer to it. |
name | string | required | The label drawn beside the socket, and the accessible name of its widget. |
type | string | required | A key in the socket type table. Sets the colour, the compatibility and the default widget. |
widget | WidgetType | false | from the type | The widget to show. false shows none. |
min | number | from the type | The lowest value of a slider. |
max | number | from the type | The highest value of a slider. |
step | number | from the type | The increment a slider snaps to, and the decimal places a slider or number field shows. |
options | string[] | none | The choices in a select. |
optionLabels | Record<string, string> | none | What each option reads as, by value, such as { png: 'PNG' }. The value is still what's stored. An option with no entry reads as its value. |
placeholder | string | none | The text an empty text, text area, number or select widget shows. |
defaultValue | unknown | none | The value the widget shows until the entity holds one. |
layout | SocketLayoutMode | 'inline' | 'inline' puts the widget beside the label. 'stacked' puts it under the label. |
rows | number | 1 | Multiplies the widget's height. |
height | number | none | The row's height in world pixels. Overrides rows. |
position | number | none | Where the dot sits, as a fraction of the entity's height from 0 to 1. The label and widget keep their row. |
Define socket types
A socket type gives every socket of that type a colour, a default widget and a rule about what it
connects to. Pass your types to KookieFlow as socketTypes:
import { KookieFlow, type Edge, type Entity, type SocketType } from '@kushagradhawan/kookie-flow';// Declared outside the component, so the object is the same on every render.const socketTypes: Record<string, SocketType> = { audio: { name: 'Audio', color: '#e5484d' }, gain: { name: 'Gain', color: '--jade-10', widget: 'slider', min: 0, max: 2, step: 0.05 }, frequency: { name: 'Frequency', color: '--sky-10', widget: 'number', step: 1 }, control: { name: 'Control', color: '--iris-10', compatibleWith: ['gain', 'frequency'] },};export function Synth({ entities, edges }: { entities: Entity[]; edges: Edge[] }) { return <KookieFlow entities={entities} edges={edges} socketTypes={socketTypes} />;}| Field | Type | Meaning |
|---|---|---|
name | string | The type's display name. |
color | string | The socket's colour. |
compatibleWith | string[] | '*' | Other types this one can connect to. '*' connects to every type. |
widget | WidgetType | The widget an input of this type shows by default. |
min, max, step | number | The values a socket of this type uses when it doesn't set its own. |
color takes a hex colour such as '#e5484d'. It also takes a colour name from the graph's own
palette, written as step 9 or step 10 of any AccentColor except gray, such as '--jade-10'.
Step 10 is adjusted for light and dark mode. '--neutral-9' and '--neutral-10' read the neutral
grey from your KookieUI theme.
Declare the table outside your component, or memoise it. A new object on every render makes the canvas rebuild every edge and socket on every render.
Compatibility
Two sockets are compatible when they have the same type, when either type is any, or when
either type lists the other in compatibleWith. The rule works in both directions, so listing
gain on control also lets a gain output connect to a control input.
In the default 'loose' connection mode, an incompatible wire still connects and is marked
invalid. In 'strict' mode, the canvas refuses it. The Connections chapter
covers both modes and the other ways to validate a wire.
The built-in socket types
DEFAULT_SOCKET_TYPES is always available, and your socketTypes are merged over it. An entry
of yours with the same key replaces the built-in entry whole, so a float you define without a
widget has no slider.
| Type | Colour | Widget |
|---|---|---|
float | blue | slider, from 0 to 1 in steps of 0.01 |
int | blue | number, with min 0, max 100 and step 1 |
string | amber | text |
enum | amber | select |
boolean | orange | checkbox |
color | pink | color |
image, mask | purple | none |
mesh | violet | none |
signal | cyan | none |
latent, model, conditioning, clip, vae | teal | none |
any | neutral grey | none |
A socket whose type isn't in the table draws in the colour of any.
Show a widget on an unconnected input
An input shows a widget when its socket names one in widget, or when its socket type has a
default widget. Only inputs have widgets. When an edge lands on the input, the widget disappears
and the input takes its value from the edge.
The socket's own settings win over the type's. min, max, step and dimensions fall back to
the socket type, while options, placeholder, defaultValue and rows come only from the
socket. Set widget: false on a socket to show no widget even when its type has one.
WidgetType | What a person does | Value |
|---|---|---|
slider | Presses or drags along the track. The value snaps to step. | number |
number | Types into a number field. | number, or '' when the field is empty |
text | Types into a text field. | string |
textarea | Types into a multi-line field. The node shows the first line. | string |
select | Picks from a list of options. A select with no options doesn't open. | string |
checkbox | Presses to toggle. | boolean |
color | Picks a colour in the browser's colour picker. | string, such as '#3e63dd' |
switch | Presses anywhere on the row to toggle. | boolean |
segmented | Presses one of the options, shown side by side. Best with two to four. | string |
vector | Drags a component sideways to change it, or clicks it to type. dimensions sets 2, 3 or 4 components. | number[] |
seed | Types a whole number, or presses the die to roll one between min and max. | number |
The canvas draws every built-in widget itself. A text, text area, number or seed widget, or one component of a vector, opens a real input over itself while a person edits it, and closes it when they press Enter or Escape or click elsewhere. In a text area, Enter adds a new line. Built-in widgets aren't drawn, and can't be pressed, below 40% zoom.
showWidgets on KookieFlow defaults to true. Set it to false to hide every widget on the
canvas, including the ones you supply.
Lay out a widget row
An inline row puts the socket's name on the left and the widget to its right. The widget starts
after a gutter of socketLabelWidth, which defaults to 96 (SOCKET_LABEL_WIDTH), and the canvas
shortens a socket name that doesn't fit the gutter. Widen the gutter when your names are long:
<KookieFlow entities={entities} edges={edges} socketLabelWidth={140} />A stacked row puts the name above the widget and gives the widget the full width of the body. Use it for a text area, or for any widget that needs room:
{ id: 'prompt', name: 'Prompt', type: 'string', layout: 'stacked', widget: 'textarea', rows: 4, placeholder: 'Describe the image',}rows multiplies the widget's height, in either layout. height sets the whole row's height in
world pixels and takes precedence over rows.
showSocketLabels defaults to true. Set it to false to hide socket names on every entity.
Read widget changes
onWidgetChange reports every change a person makes, with the entity id, the socket id and the
new value. A text field reports on every keystroke, and a slider reports on every pointer move
while it's dragged.
A widget reads its value from entity.data.values, keyed by socket id, and falls back to the
socket's defaultValue. Write each change back there:
'use client';import { useCallback } from 'react';import { KookieFlow, useGraph, type Entity } from '@kushagradhawan/kookie-flow';export function Editor({ initialEntities }: { initialEntities: Entity[] }) { const { entities, edges, setEntities, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities, }); const onWidgetChange = useCallback( (entityId: string, socketId: string, value: unknown) => { setEntities((current) => current.map((entity) => { if (entity.id !== entityId) return entity; const values = entity.data.values instanceof Object ? entity.data.values : {}; return { ...entity, data: { ...entity.data, values: { ...values, [socketId]: value } } }; }) ); }, [setEntities] ); return ( <KookieFlow entities={entities} edges={edges} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} onWidgetChange={onWidgetChange} /> );}A built-in widget draws the value it finds in data.values, so a change you don't write back can
disappear from the widget on its next redraw. For an unconnected input with a built-in widget,
onEvaluate reads the value the widget shows, as the Evaluation chapter
describes.
Custom controls
Socket widgets render in WebGL. Set widget to a supported WidgetType, or false to hide it.
Inline React components, widgetTypes and ThemeComponent are no longer supported.
To draw a new kind of node control, implement it in the WebGL renderer package. App toolbars and
inspectors can use ordinary React components outside the graph and update data.values.