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

Selection toolbar

Show a floating toolbar above the selection with built-in controls for text, comments, images and ink, or with controls you write yourself.

The selection toolbar is a row of glass controls that floats above whatever is selected. It holds controls for the selection: a font size for text, a colour for a sticky note, a width for a pen stroke, or buttons you write yourself. You decide which entity types get a toolbar and what goes in it.

Show the toolbar

Render Toolbar as a child of KookieFlow, then give each entity type that should have a toolbar a toolbar entry in entityTypes. A type without an entry shows no toolbar.

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

'use client';import { Button } from '@kushagradhawan/kookie-ui-react';import {  KookieFlow,  Toolbar,  useGraph,  type Entity,  type EntityTypeDefinition,} from '@kushagradhawan/kookie-flow';// Outside the component, so the table keeps one identity across renders.const entityTypes: Record<string, EntityTypeDefinition> = {  text: { type: 'text', toolbar: true },  comment: { type: 'comment', toolbar: true },  step: {    type: 'step',    toolbar: {      // Align and distribute, then one control of your own.      defaults: ['arrange'],      extra: ({ entities, update }) => {        const flagged = entities.every((entity) => entity.data.status === 'warning');        const toggle = () =>          entities.forEach((entity) => update(entity.id, { status: flagged ? undefined : 'warning' }));        return (          <Button size="2" emphasis="quiet" onClick={toggle}>            {flagged ? 'Clear flag' : 'Flag'}          </Button>        );      },    },  },};const initialEntities: Entity[] = [  { id: 'title', type: 'text', position: { x: 40, y: 40 }, width: 320, data: { content: 'Image pipeline' } },  { id: 'note', type: 'comment', position: { x: 440, y: 30 }, width: 220, height: 90, data: { content: 'Drag a box around all three steps.' } },  { id: 'load', type: 'step', position: { x: 40, y: 180 }, data: { label: 'Load' } },  { id: 'resize', type: 'step', position: { x: 340, y: 230 }, data: { label: 'Resize' } },  { id: 'export', type: 'step', position: { x: 640, y: 200 }, data: { label: 'Export' } },];export default function ToolbarExample() {  const { entities, edges, onEntitiesChange, onEdgesChange } = useGraph({ initialEntities });  return (    <div style={{ width: '100%', height: 400 }}>      <KookieFlow        entities={entities}        edges={edges}        entityTypes={entityTypes}        onEntitiesChange={onEntitiesChange}        onEdgesChange={onEdgesChange}      >        <Toolbar />      </KookieFlow>    </div>  );}

Every control in the toolbar edits the graph through onEntitiesChange, the same callback a drag uses. Pass it: without it, a control shows the current value, and changing the value does nothing. useGraph applies these changes for you, as the example does.

Toolbar has to sit inside KookieFlow, because it reads the selection from the flow's store. Rendered anywhere else, it throws.

The toolbar centres itself over the bounding box of the selection, 8 pixels above it. When that would push it past the top of the canvas, it sits 8 pixels below the selection instead. It follows the selection while you pan and zoom, and it hides while you drag, resize, draw a wire or drag a selection box, then comes back when the gesture ends.

How it looks

The toolbar has no card around it. It is a KookieUI Toolbar row, and every control in it is glass, so each one draws its own capsule over the canvas: a segmented control, a field, or a ToolbarGroup holding buttons that belong together. There are no dividers; the space between capsules separates them. The row tells its controls that the canvas passes behind them, so they show your theme's glass material. The whole row is one tab stop, and the arrow keys move inside it.

<KookieFlow entities={entities} edges={edges} onEntitiesChange={onEntitiesChange}>  <Toolbar size="2" /></KookieFlow>
PropTypeDefaultWhat it does
sizeToolbarProps['size'] from @kushagradhawan/kookie-ui-react'3'The size every control in the toolbar takes, unless a control states its own.
childrenToolbarRenderFnnoneReplaces the toolbar's contents for every selection. See Replace the toolbar's contents.

Turn on the built-in controls

Set toolbar: true on a built-in entity type to show its default controls. Text, comments, images and ink each have a set.

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

A control shows the value of the first selected entity. A change applies to every selected entity at once, in one onEntitiesChange call. Number fields commit when you press Enter or move focus away, so typing "24" doesn't write a font size of 2 on the way.

WidgetDefault forControlWhat it writes
sizingModetextAuto width, auto height or fixeddata.sizingMode, and the width and height that mode gives the text
fontSizetext, commentNumberdata.fontSize
noteColorcommentA hue: yellow, orange, red, pink, violet, blue, cyan, green or greydata.color, and clears data.backgroundColor and data.textColor
backgroundColornoneColour pickerdata.backgroundColor
fontFamilytextSystem, Serif or Monodata.fontFamily: 'system-ui', 'serif' or 'monospace'
fontWeighttextRegular, Semibold or Bolddata.fontWeight: 400, 600 or 700
textColortextColour pickerdata.textColor
textAligntextLeft, centre or rightdata.textAlign
lineHeighttextNumberdata.lineHeight
letterSpacingtextNumberdata.letterSpacing
objectFitimageFill, cover or containdata.objectFit
aspectLockimageLock toggledata.aspectLocked
strokeColordrawColour pickerdata.strokeColor
strokeWidthdrawNumberdata.strokeWidth, and a box refitted to the new width
arrangenoneAlign and distribute buttonsNew positions, reported as a drag reports them

A comment's toolbar picks its hue and its font size. The hue tints the note's fill, edge and text together and follows light and dark mode. List backgroundColor or textColor to also offer a colour picker for a note that needs a colour no hue gives; the pickers start from the colour the note is painted in. The widget types also include color, which draws no control.

arrange works on any entity type. It shows six align buttons once two or more entities are selected, and two distribute buttons once three or more are. With one entity selected it shows nothing. The chapter on arranging entities describes what each button does, and the keys that do the same.

The widget names are typed in groups: TextToolbarWidget, CommentToolbarWidget, ImageToolbarWidget, DrawToolbarWidget and CommonToolbarWidget (color and arrange). ToolbarWidget is all of them together.

The toolbar field's own type is ToolbarConfig. It accepts true, false, an array of widget names, an object with defaults and extra, or a render function.

Pick which controls appear

Pass an array instead of true to choose the controls and their order. Each one stands on its own in the row.

const entityTypes: Record<string, EntityTypeDefinition> = {  text: { type: 'text', toolbar: ['fontSize', 'fontWeight', 'textColor', 'arrange'] },};

Only text, comments, images and ink have default controls. On any other type, including the types you define, toolbar: true shows no toolbar, and an array is how you give that type built-in controls. A list such as ['arrange'] works on any type. With only one entity selected it shows nothing, because arrange has nothing to show for one entity.

To show no toolbar for a type, set toolbar: false or leave toolbar out.

Add your own controls

Pass an object with defaults and extra to keep built-in controls and add yours after them. defaults takes true or an array of widget names. extra is a function that returns the controls to add after them.

Build your controls from KookieUI's toolbar parts. A ToolbarButton joins the row's keyboard, where a plain Button would be a second tab stop. Put buttons that belong together in a ToolbarGroup with backdrop, so they share one glass capsule like the built-in controls.

import { ToolbarButton, ToolbarGroup } from '@kushagradhawan/kookie-ui-react';import type { EntityTypeDefinition } from '@kushagradhawan/kookie-flow';const entityTypes: Record<string, EntityTypeDefinition> = {  step: {    type: 'step',    toolbar: {      defaults: ['arrange'],      extra: ({ entities, update }) => (        <ToolbarGroup backdrop>          <ToolbarButton onClick={() => entities.forEach((entity) => update(entity.id, { status: 'warning' }))}>            Flag          </ToolbarButton>        </ToolbarGroup>      ),    },  },};

The function receives an object of type ToolbarRenderProps:

FieldTypeWhat it is
entitiesEntity[]Every selected entity.
update(entityId: string, data: Partial<EntityData>) => voidReports one data change for one entity through onEntitiesChange.
bounds{ x: number; y: number; width: number; height: number }The selection's bounding box, in world coordinates.
align(edge: AlignEdge) => voidLines the selection up on one edge or centre line, and reports the moves.
distribute(axis: DistributeAxis) => voidSpaces the selection evenly along an axis, and reports the moves.

update sends only the fields you pass. useGraph merges them into the entity's existing data, so update(id, { status: 'warning' }) keeps the label. If you apply changes yourself, merge the same way. Each call to update is its own onEntitiesChange call, so a loop over five entities reports five.

ToolbarRenderFn is the type of this function: (props: ToolbarRenderProps) => React.ReactNode.

Replace the toolbar's contents

Set toolbar to a function to draw that type's toolbar entirely yourself. The toolbar shows no built-in controls, only what your function returns.

const entityTypes: Record<string, EntityTypeDefinition> = {  step: {    type: 'step',    toolbar: ({ entities, update }) => (      <ToolbarGroup backdrop>        <ToolbarButton onClick={() => update(entities[0].id, { label: 'Renamed' })}>Rename</ToolbarButton>      </ToolbarGroup>    ),  },};

To replace the contents for every selection, whatever its types, pass the same kind of function as the child of Toolbar. That function ignores every toolbar entry in entityTypes, and the toolbar appears for any selection that holds at least one entity.

Show controls for a mixed selection

The toolbar entries in entityTypes apply only when every selected entity has the same type. When a selection mixes types, such as a text block and a step, no entry applies and no toolbar appears.

To offer controls for a mixed selection, pass a function as the child of Toolbar and decide from entities what to show. Return null to show nothing.

board.tsx
'use client';import { ToolbarButton, ToolbarGroup } from '@kushagradhawan/kookie-ui-react';import { KookieFlow, Toolbar, useGraph } from '@kushagradhawan/kookie-flow';export function Board() {  const { entities, edges, onEntitiesChange, onEdgesChange } = useGraph();  return (    <div style={{ height: 480 }}>      <KookieFlow entities={entities} edges={edges} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange}>        <Toolbar>          {({ entities: selected, align }) =>            selected.length < 2 ? null : (              <ToolbarGroup backdrop>                <ToolbarButton onClick={() => align('left')}>Align left</ToolbarButton>                <ToolbarButton onClick={() => align('top')}>Align top</ToolbarButton>              </ToolbarGroup>            )          }        </Toolbar>      </KookieFlow>    </div>  );}

align and distribute move the selection in the canvas and then report every move through onEntitiesChange, so a controlled graph keeps the new positions.

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
  • Show the toolbar
  • How it looks
  • Turn on the built-in controls
  • Pick which controls appear
  • Add your own controls
  • Replace the toolbar's contents
  • Show controls for a mixed selection