Entity styling
Choose the size, variant, corner radius and title placement every node is drawn with, mark nodes with a line of colour, and override the resolved style where you need to.
Six props on KookieFlow set how every node on the board is drawn: size, variant, radius,
header, accentHeader and entityStyle. Each one applies to the whole board. The only
per-entity setting is color, which marks one node with a line of colour. The colours, padding
and corners these props pick come from the theme's tokens, so the nodes keep matching your app in
light and dark mode.
Arrow keys move between nodes. Press Enter to reach the focused node's controls, and Escape to come back here.
'use client';import { useState } from 'react';import { KookieFlow, useGraph, type Entity } from '@kushagradhawan/kookie-flow';import type { EntitySize, EntityVariant, HeaderPosition } from '@kushagradhawan/kookie-flow';const image = { name: 'Image', type: 'image' };const initialEntities: Entity[] = [ { id: 'load', type: 'default', position: { x: 0, y: 0 }, data: { label: 'Load image' }, outputs: [{ id: 'out', ...image }] }, { id: 'blur', type: 'default', position: { x: 300, y: 0 }, color: 'teal', data: { label: 'Blur' }, inputs: [{ id: 'in', ...image }, { id: 'radius', name: 'Radius', type: 'float' }], outputs: [{ id: 'out', ...image }] }, { id: 'save', type: 'default', position: { x: 600, y: 0 }, data: { label: 'Save' }, inputs: [{ id: 'in', ...image }] },];function Choice<T extends string>(props: { value: T; options: T[]; onChange: (value: T) => void }) { const pick = (value: string) => props.options.find((option) => option === value); return ( <select value={props.value} onChange={(e) => { const next = pick(e.target.value); if (next) props.onChange(next); }}> {props.options.map((option) => <option key={option}>{option}</option>)} </select> );}export default function EntityStylingExample() { const [size, setSize] = useState<EntitySize>('2'); const [variant, setVariant] = useState<EntityVariant>('surface'); const [header, setHeader] = useState<HeaderPosition>('inside'); const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities, initialEdges: [ { id: 'e1', source: 'load', sourceSocket: 'out', target: 'blur', targetSocket: 'in' }, { id: 'e2', source: 'blur', sourceSocket: 'out', target: 'save', targetSocket: 'in' }, ], }); return ( <div style={{ width: '100%' }}> <div style={{ display: 'flex', gap: 12, paddingBottom: 12 }}> <Choice value={size} options={['1', '2', '3', '4', '5']} onChange={setSize} /> <Choice value={variant} options={['surface', 'outline', 'soft', 'classic', 'ghost']} onChange={setVariant} /> <Choice value={header} options={['inside', 'outside', 'none']} onChange={setHeader} /> </div> <div style={{ height: 360 }}> <KookieFlow entities={entities} edges={edges} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} size={size} variant={variant} header={header} defaultViewport={{ x: 32, y: 72, zoom: 0.75 }} /> </div> </div> );}| Prop | Type | Default | What it sets |
|---|---|---|---|
size | EntitySize | '2' | Padding, socket row and widget heights, and the corner when radius is unset |
variant | EntityVariant | 'surface' | The body's fill, border and shadow |
radius | EntityRadius | Follows size | The corner of the body and of its widgets |
header | HeaderPosition | 'inside' | Where the title is drawn, if at all |
accentHeader | boolean | false | A line of the theme's accent along the top edge of every node |
entityStyle | Partial<EntityStyleOverrides> | None | Colour, border, corner and shadow overrides |
These props style nodes. Text, images, video, models, comments, reroutes and ink draw themselves, so a title, a body fill and an accent line never appear on them.
Pick a size
size takes '1', '2', '3', '4' or '5'. It picks a step on KookieUI's surface tokens
for the padding (--surface-p-N) and the corner (--radius-surface-N), and the same step on its
control tokens for the height and inset of widgets. Sizes '1' and '2' share a step.
size | Surface and control step |
|---|---|
'1' | 1, with smaller sockets, so socket rows can sit closer together |
'2' | 1 |
'3' | 2 |
'4' | 3 |
'5' | 4 |
The default '2' uses KookieUI's first step, one below a page card's default. That's a judgment:
a node is narrower than a page card, and you usually read a board zoomed out, so the page's
default step spends too much of the node on padding.
size doesn't change the text. Titles and socket labels stay at a 12-pixel font size in world
units, and zooming is what makes them larger.
Pick a variant
variant sets how the body is painted.
variant | Fill | Border | Shadow |
|---|---|---|---|
'surface' | --neutral-1 in light, --neutral-3 in dark | --neutral-5 | A long, soft shadow |
'outline' | None | --neutral-5 | None |
'soft' | --neutral-2 in light, --neutral-3 in dark | None | A long, soft shadow |
'classic' | --color-surface | None | A short drop shadow |
'ghost' | None | None | None |
A 'ghost' node draws no body at all, so its title, sockets and widgets sit straight on the
canvas.
Node shadows don't read the Theme's depth setting, so a depth="flat" Theme still shows them.
For a flat board, use 'outline', or set entityStyle.shadow to 'none'.
Set the corner radius
radius takes 'none', 'small', 'medium', 'large' or 'full'. From 'small' to
'full', the body reads --radius-surface-1 to --radius-surface-4, and 'none' is a square
corner. The Theme's own radius setting decides what each of those tokens is worth.
Leave radius unset and the corner follows size: 'small' at sizes '1' and '2', then
'medium', 'large' and 'full' for '3', '4' and '5'. The same corner reaches the
selection outline and every image, video and model on the board.
'full' is the largest card corner, not a pill, because the surface tokens stop at a rounded
card.
Widgets read KookieUI's control corners instead. With radius set, 'small', 'medium' and
'large' read --radius-1 to --radius-3, and 'full' reads --radius-full. The Theme's own
radius level decides what those are worth, so under a Theme at radius="full", KookieUI's
default, every one of them draws a pill, and under radius="none" every one is square. With
radius unset, a widget takes the Theme's control corner at the node's size.
Place the title
header decides where a node's title goes.
'inside'draws the title in a band at the top of the body, above the first socket row.'outside'draws the title above the body. The title isn't inside the node's box, so leave room above each node for it.'none'draws no title and reserves no space for one.
The title is the entity's data.label, or its type when there's no label. A title that's
wider than the node ends in an ellipsis. It's drawn in the semibold weight of the canvas font,
which the Fonts and text rendering chapter covers.
Mark nodes with colour
There are two ways to mark nodes with colour, and both draw the same thin line inside the top edge of the body:
accentHeaderdraws the line on every node, in the theme's--accent-9, whateverheaderis.coloron an entity draws the line in a hue you choose, for that node only. It replaces theaccentHeaderline on that node.
pipeline.tsximport type { Entity } from "@kushagradhawan/kookie-flow";const entities: Entity[] = [ { id: "load", type: "default", position: { x: 0, y: 0 }, color: "teal", data: { label: "Load image" }, outputs: [{ id: "image", name: "Image", type: "image" }], }, { id: "upscale", type: "default", position: { x: 300, y: 0 }, color: "orange", data: { label: "Upscale" }, inputs: [{ id: "image", name: "Image", type: "image" }], },];color takes an AccentColor: gray, gold, bronze, brown, yellow, amber, orange,
tomato, red, ruby, crimson, pink, plum, purple, violet, iris, indigo,
blue, cyan, teal, jade, green, grass, lime, mint or sky. These hues ship with
the package and look the same in light and dark mode, except gray, which reads the theme's
--neutral-9. The theme's accent doesn't change them. A red node isn't your destructive
colour either, so it stays the same red if your theme's destructive colour changes.
The line needs a body to sit on. It appears on 'surface', 'soft' and 'classic' nodes, and
'outline' and 'ghost' nodes draw no line. A name that isn't in the list logs one warning,
and the node draws as if it had no colour. color doesn't tint the body, the selection outline
or the widgets.
Override the resolved style
entityStyle replaces individual values that the other props resolved. It applies to every node.
| Field | Type | What it replaces |
|---|---|---|
background | string | The body fill. Any CSS colour. |
borderColor | string | The border colour. Any CSS colour. |
borderWidth | number | The border width, in pixels |
borderRadius | number | The body corner, in pixels. It wins over radius and size, and leaves widget corners alone. |
shadow | '1' to '6', or 'none' | The shadow |
Write a colour as a token, such as var(--neutral-4). The canvas resolves it inside the Theme,
and resolves it again whenever the theme changes, so the override follows light and dark mode.
A hex value stays the same in both modes.
board.tsx<KookieFlow entities={entities} edges={edges} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange} variant="soft" radius="medium" entityStyle={{ background: "var(--neutral-4)", borderColor: "var(--accent-9)", borderWidth: 1, shadow: "none", }}/>Some overrides depend on the variant:
backgroundhas no effect on'outline'or'ghost', because those variants draw no fill.borderColorshows only where the border has a width.'soft','classic'and'ghost'have none until you setborderWidth.shadowfrom'1'to'5'picks one of five fixed drop shadows, from small to large.'6'draws no shadow.
You can write the object inline. The canvas compares its five fields, so a new object with the same values doesn't re-resolve any styles.