Options
The object passed to createKlad (or the options prop on the Vue and React components). Only data is required — everything below has a default that produces a usable chart.
Data
| Option | Type | Default | |
|---|---|---|---|
data | NodeData[] | — | Flat array. Every item is { id, parentId?, ...yours }; an unresolvable parentId makes a root and emits a warning. |
nodeSize | Size | (item, at) => Size | { w: 180, h: 64 } | The box each node occupies. Declared, never measured — see Sizing. Exported as DEFAULT_NODE_SIZE. |
label | (item, at) => string | name → label → title → id | The text the canvas draws inside a node, independent of whatever your card renders. Return '' for a node that should stay blank. |
Layout
The shape the tree is drawn in, and the knobs that tune it. All of these can also be changed after construction with setLayoutOptions — see Layouts for what each shape is for.
| Option | Type | Default | |
|---|---|---|---|
layout | 'tidy' | 'file' | 'radial' | 'sunburst' | 'tidy' | Which shape. tidy is the tiered chart; file indented rows; radial concentric rings; sunburst nested arcs. |
edgeStyle | 'tiered' | 'folder' | 'spoke' | 'bezier' | 'none' | per-layout | The line drawn between a parent and a child, overriding the one the layout would pick — see Choosing the connector. |
layoutStep | number | derived | The per-level step, whose meaning is per-layout: the file indent, the radial/sunburst ring size. Omitted, each layout derives one from your nodeSize. |
rowGap | number | spacing.y | file only: the gap between consecutive rows. |
maxRings | number | 3 | sunburst only: how many rings are drawn around the centre. Deeper nodes are still there — drilling in reveals them. |
centre | string | null | null | sunburst only: the id at the middle of the wheel. Changing it animates; see setCentre. |
colourBranches | boolean | per-layout | Fill nodes by which top-level branch they belong to, from theme.palette. On for sunburst, off elsewhere. |
orientation | 'tb' | 'bt' | 'lr' | 'rl' | 'tb' | Which way the tree grows. tidy only — a file list is a vertical list of rows whatever you set, and a wheel has no reading direction. |
rtl | boolean | false | Mirrors sibling order; the growth direction is unaffected. |
spacing | { x?, y?: number } | { x: 16, y: 48 } | Gaps between siblings and between levels, in world units. |
collapsedByDefault | boolean | (item, at) => boolean | false | Which nodes start closed. Often a question about depth — see Where a node sits. |
Content
| Option | Type | Default | |
|---|---|---|---|
renderNode | (element, context) => void | — | Draws your own card. See Node content. Vue and React use the #node slot and the render prop instead. |
lodThresholds | { block: number; label: number } | { block: 0.25, label: 0.6 } | The zoom levels at which the canvas switches between a plain shape, a labelled box, and overlay cards. |
Appearance
| Option | Type | Default | |
|---|---|---|---|
theme | Partial<Theme> | — | Colours and weights the canvas draws with. See Theme. |
minimap | boolean | MinimapOptions | false | { position, width, height, silhouetteColour }. silhouetteColour is the one piece your own CSS cannot restyle — set it for a dark host. |
zoomLimits | { minK, maxK: number } | { minK: 0.05, maxK: 4 } | The floor is lowered automatically — never raised — when the tree is wider than the viewport, so fit() can always show everything. |
Behaviour
| Option | Type | Default | |
|---|---|---|---|
maxChildren | number | ((item) => number) | — | How many children a node draws before the rest are rolled into one node saying how many it stands for. Per parent. See Very wide levels. |
pinChildren | (item, at) => boolean | — | Children shown whatever the cap says — your working set. Pins precede the budget rather than being part of it. |
mayHaveChildren | (item, at) => boolean | — | Whether a node has children, whether or not they are in data yet. Only consulted for nodes with none; ignored without loadChildren. See Children on demand. |
loadChildren | (item) => NodeData[] | Promise<NodeData[]> | — | Fetches one node's children the first time it is opened. The chart keeps what you return. |
dragAndDrop | boolean | false | Dragging a node — or the whole selection, if it is in one — onto a new parent, or between two siblings. Reported through nodeDrop before it is applied. See Drag and drop. |
canMove | (event) => boolean | — | Your rule on whether a move is allowed — asked during the drag, at the drop, and by move(). See A rule of your own. |
history | number | false | 100 | How many edits undo can walk back. false turns it off, for an app with its own undo stack. See Undo, redo. |
keyboardEditing | boolean | false | Reorder, indent, outdent and delete the focused node from the keyboard. See Without a pointer. |
edgeFlow | (parent, child) => boolean | — | Which connectors are drawn as a travelling dash. Keeps the chart redrawing — see Edges that flow. |
selection | boolean | false | Selecting nodes with the pointer — click, ctrl/cmd-click, shift-click, shift-drag for a box, alt-drag for a lasso. select() and selectionChange work either way; this is only about the pointer. |
keyboard | boolean | true | Camera control from the keyboard, and the tab stop that makes the chart reachable at all — see Navigating. |
animate | boolean | true | Every animation this layer starts on its own: the expand/collapse transition, camera eases, kinetic panning. prefers-reduced-motion: reduce forces it off regardless. |
autoPanOnToggle | boolean | true | Keeps the toggled node pinned on screen while the layout moves around it. |
ring | boolean | true | The one-shot confirmation flash after a single-node toggle. |
toggleOnNodeClick | boolean | false | Tapping a node's body expands or collapses it. For cards with no room for a toggle button. |
worker | boolean | true | Renders in a Web Worker. Falls back to the main thread on its own — a CSP that blocks workers, a canvas whose context was already taken — with a warning, never a failure. |
Choosing the connector
Each layout comes with the line that reads correctly on it: an org chart's elbow, a file list's guide line down the gutter, a wheel's spoke. edgeStyle overrides that when your chart wants a different answer.
createKlad(el, { data, edgeStyle: 'spoke' }) // tidy, but straight lines
createKlad(el, { data, edgeStyle: 'none' }) // no connectors at all'tiered' | Down, across, down — the org chart elbow. The default everywhere except the three below. |
'folder' | A guide line down the indent gutter. What file uses. |
'spoke' | Straight, centre to centre. What radial uses. |
'bezier' | The same two ends as 'tiered', curved instead of bent. The one style no layout asks for. |
'none' | Nothing is drawn between nodes. What sunburst uses, since its arcs already touch. |
Leaving it out is right almost every time — a folder guide line on a tiered chart is not a style choice, it is a mistake. Reach for it when your own cards already carry the structure and the lines are noise, or when a wide tidy tree reads better with straight lines than with elbows.
'none' is also the cheapest: the engine skips building the edge index and its quadtree entirely rather than building one for the renderer to ignore.
One thing it does not take away. On a tiered chart the "there is more inside" mark — the short stub and dot below a collapsed node — stays whatever style you choose, because a branch still continues there whether or not a line is drawn to it, and at the zoom where the cards and their toggles are gone it is the only thing that says so. The exception is 'folder', which drops it on purpose: a file row has a chevron beside its name, and a stub underneath would say the same thing twice.
Edges that flow
createKlad(el, {
data,
edgeFlow: (parent, child) => child.status === 'active',
})A travelling dash, for a branch that is live — a flow, a dependency, a route that is carrying something. Asked once per node whenever the data changes, never per frame. An edge is named by its child, since every node has exactly one parent.
It keeps the chart drawing, and that is the point of it being a predicate. Everything else here renders only when something changes; an idle chart costs nothing at all. A travelling dash has to advance every frame, so for as long as one marked edge is in the visible tree the loop keeps going. Marking one branch is cheap. Marking everything is a decision about somebody's battery — so the option asks you which, rather than offering a switch.
Collapse a branch and its edges stop counting: out of the visible tree is out of the animation. So does zooming out — past the block threshold (see lodThresholds) a connector is a couple of pixels wide, the dash is smaller than one, and dashed stroking is not free. There they are drawn as ordinary lines and the chart goes still.
Measured on 20,000 nodes with every edge flowing, which is not a sensible setting: 19 frames in 400ms close up, and 0 zoomed out.
Colour, weight, dash pattern and speed are theme tokens — edgeFlowStroke, edgeFlowWidth, edgeFlowDash, edgeFlowSpeed. The dash is in screen pixels and does not scale with zoom, so it stays legible exactly when the chart gets busy enough to need it.
Exports draw these as ordinary connectors. A dash frozen mid-travel in a PNG is just an odd-looking gap.
Reduced motion is yours to honour, not the chart's to assume: whether a flow still means anything standing still depends on what you are using it for. Read the media query and drop the predicate.
const still = matchMedia('(prefers-reduced-motion: reduce)').matches
createKlad(el, { data, ...(still ? {} : { edgeFlow: isLive }) })Where a node sits
Every per-node option gets a second argument saying where in the tree the node is:
createKlad(el, {
data,
collapsedByDefault: (item, at) => at.depth > 2,
})interface NodePlace {
depth: number // distance from a root; a root is 0
index: number // its slot among its own siblings, in data order
siblings: number // how many siblings it has, counting itself
parent: NodeData | null // the parent's data, or null for a root
}nodeSize, label, collapsedByDefault, mayHaveChildren and pinChildren all receive it.
A flat { id, parentId } array does not say what depth anything is at, so the alternative was walking parent links yourself — once per node, per data change. And the option that most often wants depth is the one you could not answer anyway: collapsedByDefault runs against rows that may have arrived from loadChildren, which are in no array you hold.
Every field is about the node's place in your data, not on screen. Depth is the same whether the chart is drawn tiered, indented or as a wheel, and it does not change when a branch is collapsed or a filter hides its siblings — an option that answered differently once something was folded away would give a different result the moment you unfolded it.
Types
type NodeData = { id: string; parentId?: string | null; [key: string]: unknown }
type Size = { w: number; h: number }
type Orientation = 'tb' | 'bt' | 'lr' | 'rl'
type Camera = { x: number; y: number; k: number }NodeData, NodePlace, Size, Orientation, Camera, Bounds, Theme, LodThresholds, ZoomLimits and Warning are all re-exported from the binding you installed — you never have to reach past it into the core to name something it already hands you.