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

Canvas and camera

Pan and zoom the canvas, set where the view starts, move the camera from code, and convert between screen and world coordinates.

The canvas extends without limit in every direction, and the camera's position sets which part of it is on screen. That position is a Viewport: x and y, an offset in screen pixels, and zoom, the scale. You set where the camera starts with props, people move it with the pointer, and you move it from code through the ref.

Pan and zoom

The canvas responds to these gestures without any setup.

GestureWhat it does
Scroll wheelZooms toward the pointer.
Two-finger scroll on a trackpadZooms toward the pointer, because the browser reports it as a wheel event.
Drag with the middle mouse buttonPans.
Hold Space and drag with the primary buttonPans. The canvas needs keyboard focus, which any press on it gives.
Two fingers on a touch screenPinch to zoom around the point between them, and move both fingers to pan.

The wheel always zooms. There's no scroll-to-pan: an upward or downward swipe on a trackpad zooms, and a sideways swipe does nothing. The canvas also cancels the page's own scrolling while the pointer is over it, so a long page that embeds a canvas doesn't scroll until the pointer leaves the canvas.

One finger on a touch screen behaves like the mouse's primary button: it selects, drags, and draws a selection box, as the Selection chapter describes. When you drag a node or a wire to within 50 pixels of the canvas's edge, the camera pans toward that edge until you move back.

Set the starting view

defaultViewport sets where the camera starts. x and y are where the world origin sits, in pixels from the canvas's top-left corner, and zoom is the scale.

<KookieFlow  entities={entities}  edges={edges}  // The world origin 120px right and 80px down, at 75%.  defaultViewport={{ x: 120, y: 80, zoom: 0.75 }}/>
PropTypeDefaultMeaning
defaultViewportViewportDEFAULT_VIEWPORT, which is { x: 0, y: 0, zoom: 1 }The camera's position when the canvas mounts.

The canvas uses defaultViewport only when it mounts. Changing the prop later does nothing, so call setViewport on the ref to move the camera after that.

You rarely know the canvas's size when you write the starting view. To frame the graph instead, call fitView in an effect after mount, as the toolbar example below does. To restore a saved view, pass the viewport from toObject() back in as defaultViewport; the Saving and exporting chapter covers that round trip.

Limit the zoom

minZoom and maxZoom bound the wheel and pinch gestures.

PropTypeDefaultMeaning
minZoomnumber0.01The furthest out a person can zoom. fitView also uses it as its lower bound.
maxZoomnumber16The furthest in a person can zoom.

The limits apply to gestures, not to the ref. setViewport and setCenter take any zoom you give them, and zoomIn and zoomOut stop only at 0.01 and 16. Clamp the value yourself when code sets the zoom.

The canvas stops drawing some detail as you zoom out, which is worth knowing when you choose minZoom. Below 0.4 a node's built-in widgets aren't drawn and can't be pressed. Socket labels disappear below 0.35, edge labels below 0.25, and node titles below 0.15.

Move the camera from code

The ref on <KookieFlow> carries six camera methods. None of them is reported through a callback, and there's no prop that tells you the camera moved. To follow the camera as it moves, subscribe to viewport in the flow store, as the chapter on the flow store shows.

MethodSignatureWhat it does
fitView(options?: FitViewOptions) => voidFrames every entity, or the ones you list. Does nothing when there's nothing to frame.
setCenter(x: number, y: number, options?: { zoom?: number }) => voidPuts the world point in the middle of the canvas, at options.zoom or the current zoom.
zoomIn(step?: number) => voidAdds step to the zoom. The default step is 0.25.
zoomOut(step?: number) => voidSubtracts step from the zoom. The default step is 0.25.
getViewport() => ViewportReturns the camera's current position.
setViewport(viewport: Viewport) => voidReplaces the camera's position.

fitView takes these options:

OptionTypeDefaultMeaning
paddingnumber50Space added around the entities before the zoom is calculated, in world units. At zoom 1 that's screen pixels.
entitiesstring[]every entityThe ids to frame. An empty list frames every entity.
minZoomnumberthe minZoom propThe furthest out the fit may go.
maxZoomnumber1The furthest in the fit may go, so a small graph is shown at full size rather than blown up.

FitViewOptions also lists duration and includeHiddenEntities, but neither has any effect. The camera always moves in one step, and entities hidden inside a collapsed frame still count toward the area it frames.

This toolbar frames the selection, or the whole graph when nothing is selected, and steps the zoom. It sits outside <KookieFlow>, so it reaches the camera through the ref.

board.tsx
'use client';import { useEffect, useRef } from 'react';import { KookieFlow, useGraph, type Entity, type KookieFlowInstance } from '@kushagradhawan/kookie-flow';export function Board({ initialEntities }: { initialEntities: Entity[] }) {  const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities });  const flowRef = useRef<KookieFlowInstance>(null);  // Frame the graph once the canvas has its size.  useEffect(() => {    flowRef.current?.fitView({ padding: 80 });  }, []);  const fitSelection = () => {    const ids = flowRef.current?.getSelectedEntities().map((entity) => entity.id) ?? [];    flowRef.current?.fitView({ entities: ids, padding: 80 });  };  return (    <div>      <div>        <button onClick={fitSelection}>Fit</button>        <button onClick={() => flowRef.current?.zoomIn()}>Zoom in</button>        <button onClick={() => flowRef.current?.zoomOut()}>Zoom out</button>      </div>      <div style={{ height: 480 }}>        <KookieFlow          ref={flowRef}          entities={entities}          edges={edges}          onEntitiesChange={onEntitiesChange}          onEdgesChange={onEdgesChange}          onConnect={onConnect}        />      </div>    </div>  );}

zoomIn and zoomOut add and subtract, so a step from 0.25 doubles the scale while a step from 4 barely changes it. They also change only zoom and leave x and y alone, so the view doesn't stay centred on what you were looking at. To zoom around the centre, pass setCenter the world point at the centre and the new zoom.

Convert between screen and world coordinates

Entity positions are in world coordinates. Pointer events and DOM layout are in screen coordinates. Two functions convert between them, given the current viewport.

FunctionSignatureMeaning
screenToWorld(screenPos: XYPosition, viewport: Viewport) => XYPositionWhere a point on the canvas lands in the world.
worldToScreen(worldPos: XYPosition, viewport: Viewport) => XYPositionWhere a world point appears on the canvas.

The conversion is screen = world × zoom + offset, with the viewport's x and y as the offset. World y grows downward, as it does in the DOM. A screen position is measured from the canvas's top-left corner, not the page's, so subtract the left and top of the canvas's getBoundingClientRect() from a pointer's clientX and clientY before you convert.

This function zooms by a factor around the middle of the canvas. It finds the world point at the centre, then puts that point back in the centre at the new zoom.

zoom-by.ts
import { screenToWorld, type KookieFlowInstance } from '@kushagradhawan/kookie-flow';export function zoomBy(flow: KookieFlowInstance, canvas: HTMLElement, factor: number) {  const { width, height } = canvas.getBoundingClientRect();  const viewport = flow.getViewport();  const centre = screenToWorld({ x: width / 2, y: height / 2 }, viewport);  // The ref doesn't apply minZoom and maxZoom, so clamp here.  const zoom = Math.min(4, Math.max(0.1, viewport.zoom * factor));  flow.setCenter(centre.x, centre.y, { zoom });}

Call it with the element that wraps <KookieFlow>, such as zoomBy(flow, wrapper, 1.25). The same conversion places a new entity in the middle of the view, or positions a DOM popover over an entity with worldToScreen(entity.position, flow.getViewport()).

Show the grid

The canvas draws a dot grid behind the graph. showGrid={false} leaves the plain canvas colour.

PropTypeDefaultMeaning
showGridbooleantrueDraws a dot every 20 world units.

A dot sits on every multiple of 20, which is where snapToGrid lands a node at its default snapGrid of [20, 20]. The grid's spacing is fixed at 20, so a snapGrid that isn't a multiple of 20 can put nodes between the dots. The Layout and alignment chapter covers snapping.

Each dot stays about the same size on screen at every zoom. The grid fades as you zoom out past 0.9 and is gone below 0.45, where the dots would be too dense to read as a grid. Its colour comes from the theme and follows light and dark mode.

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
  • Pan and zoom
  • Set the starting view
  • Limit the zoom
  • Move the camera from code
  • Convert between screen and world coordinates
  • Show the grid