Skip to main content

Events

Every callback hands you intent, not action. The component never mutates your tree, never deletes anything, never reorders. It tells you what the user asked for and gets out of the way.

That also means omitting a handler removes its control — see no handler, no control.

Selection

onSelect: (ids: TreeNodeId[], meta: SelectMeta) => void

Always fires with the complete new selection, not a delta. meta tells you how it was made:

type SelectMeta = {
multi: boolean; // Ctrl/Cmd held — add to or remove from the selection
range: boolean; // Shift held — extend from the anchor row
via: 'pointer' | 'keyboard';
};

via is useful for scroll-into-view: you generally want to scroll on keyboard selection and not on a click, since the click already happened somewhere visible.

Move

onMove: (moved: TreeNodeId[], target: TreeNodeId, position: DropPosition) => void

position is 'before' | 'inside' | 'after'. Omitting onMove disables drag entirely — no handles, no drop indicators.

moved is an array because dragging a multi-selection moves all of it.

The usual implementation is one line:

onMove={(moved, target, position) =>
setNodes((cur) => moveNodes(cur, moved, target, position))
}

moveNodes applies the descendant guard for you — dropping a folder inside its own child is rejected rather than corrupting the tree. If you write your own, use canDrop to get the same protection.

Rename

onRename: (id: TreeNodeId, name: string) => void

Fires when the inline editor commits — Enter or blur. Escape cancels and fires nothing. Omit to remove renaming.

Hidden and locked

onToggleHidden: (id: TreeNodeId) => void
onToggleLocked: (id: TreeNodeId) => void

These fire with the id only — you own the flag. The component reads node.hidden / node.locked to render state; it doesn't track its own.

Omit either and the corresponding icon isn't rendered at all.

Delete

onDelete: (ids: TreeNodeId[]) => void

:::warning It fires with intent, not permission Nothing has been removed when this fires. Whether anything is destroyed is your code's decision — confirm it, undo-stack it, or ignore it. Rows marked readOnly are excluded from ids before you see them. :::

Hover and context menu

onHoverNode: (id: TreeNodeId | null) => void
onContextMenu: (id: TreeNodeId, e: React.MouseEvent) => void

onHoverNode fires null on leave — handy for syncing a highlight with a canvas or 3D view.

onContextMenu hands you the raw event and does nothing else. There is no built-in menu; call e.preventDefault() and render your own.

Expansion

expandedIds?: TreeNodeId[]
onExpandedChange?: (ids: TreeNodeId[]) => void
defaultExpandedIds?: TreeNodeId[]

Controlled if you pass expandedIds, uncontrolled otherwise. Search auto-expands to reveal matches without permanently changing your expansion state — clear the search and it collapses back.