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

Drawing

Press D to turn on the pen and draw freehand strokes. Each stroke is an entity you can select, move, restyle and undo.

The pen draws freehand ink on the canvas. Each stroke becomes an entity of type draw, so you can select it, move it, delete it and undo it like any other entity, and you can save it and load it with the rest of the graph.

Turn on the pen

Press D while the canvas has focus to turn on the pen, and press D again or Escape to turn it off. The canvas takes focus when you click it or tab to it. While the pen is on, the pointer is a crosshair.

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

'use client';import {  KookieFlow,  Toolbar,  useGraph,  type EntityTypeDefinition,  type KookieFlowProps,} from '@kushagradhawan/kookie-flow';// Both objects live outside the component, so neither changes identity on a render.const entityTypes: Record<string, EntityTypeDefinition> = {  draw: { type: 'draw', toolbar: true },};const penStyle: KookieFlowProps['penStyle'] = { strokeWidth: 4, strokeColor: '#e5484d' };export default function DrawingExample() {  const { entities, edges, onEntitiesChange, onEdgesChange } = useGraph({    initialEntities: [      {        id: 'hint',        type: 'text',        position: { x: 40, y: 40 },        width: 420,        data: { content: 'Click the board, press D, and drag to draw. Press Escape to select.' },      },    ],    history: true,  });  return (    <div style={{ width: '100%', height: 360 }}>      <KookieFlow        entities={entities}        edges={edges}        onEntitiesChange={onEntitiesChange}        onEdgesChange={onEdgesChange}        entityTypes={entityTypes}        penStyle={penStyle}        ariaLabel="Drawing board"      >        <Toolbar />      </KookieFlow>    </div>  );}

While the pen is on, a press of the primary button starts a stroke wherever it lands, including over a node, a socket or a widget. You can still pan with the middle button or by holding Space. The pen stays on after each stroke, so you can draw several in a row. Turn it off to select or move what you drew.

D turns the pen on or off only when you press it without Cmd, Ctrl, Alt or Shift. Alt+D, for example, lines up the right edges of two or more selected entities. No prop or ref method turns on the pen, so the people using your canvas turn it on from the keyboard.

One stroke is one entity

While you draw, the canvas grows the stroke on screen and reports nothing to your app. When you release the button, the canvas does three things:

  1. Simplifies the stroke, dropping the points along straight runs and keeping the corners.
  2. Fits a box around the ink, including half the stroke's width on every side.
  3. Reports the stroke through onEntitiesChange as a single add change.

Because the stroke arrives as one change, useGraph({ history: true }) records it as one undo step, and Cmd+Z or Ctrl+Z removes the whole stroke. A tap without moving leaves a dot.

A short zigzag drawn with the default pen arrives as an entity like this one:

import type { DrawEntity } from '@kushagradhawan/kookie-flow';const stroke: DrawEntity = {  id: 'kf-draw-1757592000000-k3x9q', // generated by the canvas  type: 'draw',  position: { x: 118.5, y: 78.5 }, // the top-left corner of the box  width: 63, // the distance across the points, plus the stroke width  height: 43,  resizable: false,  data: { points: [1.5, 41.5, 31.5, 1.5, 61.5, 41.5], strokeWidth: 3 },};

DrawEntity is the type for it: an Entity<DrawEntityData> whose type is 'draw'.

DrawEntityData fieldTypeDefaultWhat it is
pointsnumber[]noneThe stroke as a flat list, x, y, x, y, relative to the entity's top-left corner.
strokeWidthnumber3The thickness of the ink, in world pixels.
strokeColorstringThe theme's primary text colourThe colour of the ink.

Points are relative, so moving a stroke changes its position and leaves points alone. The box decides what a click hits, so a click anywhere inside a stroke's box selects it, not only a click on the ink.

Set the pen's style

penStyle sets the width and colour of the strokes the pen draws next. Its type is Pick<DrawEntityData, 'strokeWidth' | 'strokeColor'>.

<KookieFlow  entities={entities}  edges={edges}  onEntitiesChange={onEntitiesChange}  penStyle={{ strokeWidth: 6, strokeColor: '#3e63dd' }}/>

Without penStyle, ink is 3 pixels wide and has no strokeColor of its own. Ink with no colour of its own is drawn in the theme's primary text colour, and it changes colour when the theme switches between light and dark. Ink with a strokeColor keeps that colour in both.

A stroke keeps the style it was drawn with. When penStyle changes, only the strokes drawn after the change use the new style.

Write strokeColor as a six-digit hex value, such as #3e63dd. The ink is drawn in WebGL, which can't resolve a CSS variable. The toolbar's colour control is the browser's colour input, which can show only a six-digit hex value.

Restyle strokes afterwards

To let people restyle ink, turn on the built-in toolbar controls for the draw type and render Toolbar inside KookieFlow, as the example above does:

const entityTypes: Record<string, EntityTypeDefinition> = {  draw: { type: 'draw', toolbar: true },};

Select a stroke, and the toolbar shows two controls. strokeColor is a colour picker that shows the stroke's colour, or the theme colour for ink with none. strokeWidth is a number field that commits when you press Enter or move focus away, and it ignores zero and negative values. Both apply to every selected stroke. The selection toolbar chapter covers the toolbar itself.

A width change reports three changes for each stroke, not one: a new position, new dimensions and new data holding the width and the shifted points. The box holds half the width on every side and the points are relative to that box, so a thicker line needs a bigger box, a corner moved out, and every point moved in by the same amount. The ink stays exactly where it was.

A colour change reports one data change for each selected stroke. To restyle strokes from your own code, report the same change. Set strokeColor to undefined to put a stroke back on the theme colour:

const strokes = flowRef.current?.getSelectedEntities().filter((e) => e.type === 'draw') ?? [];onEntitiesChange(strokes.map((e) => ({ type: 'data', id: e.id, data: { strokeColor: '#3e63dd' } })));

Create ink from data

You can put strokes in entities yourself, such as when you load a saved drawing or import a path from another tool. Build them the way the pen does, or the box and the ink disagree and clicks miss the stroke.

ink.ts
import type { DrawEntity, XYPosition } from '@kushagradhawan/kookie-flow';/** Fits a pen-style box around world points: half the stroke width of room on every side. */export function fitInk(world: number[], strokeWidth: number) {  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;  for (let i = 0; i < world.length; i += 2) {    minX = Math.min(minX, world[i]);    maxX = Math.max(maxX, world[i]);    minY = Math.min(minY, world[i + 1]);    maxY = Math.max(maxY, world[i + 1]);  }  const position: XYPosition = { x: minX - strokeWidth / 2, y: minY - strokeWidth / 2 };  return {    position,    width: maxX - minX + strokeWidth,    height: maxY - minY + strokeWidth,    points: world.map((v, i) => v - (i % 2 === 0 ? position.x : position.y)),  };}export function ink(id: string, world: number[], strokeWidth = 3, strokeColor?: string): DrawEntity {  const box = fitInk(world, strokeWidth);  return {    id,    type: 'draw',    position: box.position,    width: box.width,    height: box.height,    resizable: false,    data: { points: box.points, strokeWidth, ...(strokeColor ? { strokeColor } : {}) },  };}

ink('zigzag', [100, 200, 160, 140, 220, 200, 280, 140], 4) returns a four-pixel zigzag in the theme colour. Pass at least one point, because fitInk can't fit a box around none.

Set resizable: false, as the pen does. The canvas draws ink from its points and never scales them to the box, so dragging a corner would change the box and leave the ink as it was.

The same fitInk refits a stroke for a width control of your own. Turn the stroke's points back into world points, fit them at the new width, and report all three changes together:

import type { DrawEntity, EntityChange } from '@kushagradhawan/kookie-flow';import { fitInk } from './ink';export function setInkWidth(stroke: DrawEntity, strokeWidth: number): EntityChange[] {  const { x, y } = stroke.position;  const world = (stroke.data.points ?? []).map((v, i) => v + (i % 2 === 0 ? x : y));  const box = fitInk(world, strokeWidth);  return [    { type: 'position', id: stroke.id, position: box.position },    { type: 'dimensions', id: stroke.id, dimensions: { width: box.width, height: box.height } },    { type: 'data', id: stroke.id, data: { strokeWidth, points: box.points } },  ];}
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
  • Turn on the pen
  • One stroke is one entity
  • Set the pen's style
  • Restyle strokes afterwards
  • Create ink from data