Oihana Next UI - 0.9.0

Application logo

Sortable Tree Component

Sortable Tree (drag vertically to reorder, horizontally to indent / outdent)

Drag a row up or down to reorder among siblings, and left or right to change its depth : the pointer's horizontal position projects the target parent. Dragging a folder moves its whole subtree (collapsed during the drag), so a node can never be dropped into its own children.

src
components
Button.jsx
Input.jsx
hooks
useValue.js
index.js
public
logo.svg
README.md
<SortableTree
    defaultItems={ [ { id , label , children : [...] } ] }
    renderNode={ node => <SortableTreeItem>{ node.label }</SortableTreeItem> }
/>

Controlled Tree (live structure)

src
components
Button.jsx
Input.jsx
hooks
useValue.js
index.js
public
logo.svg
README.md
[
  {
    "src": [
      {
        "components": [
          "Button.jsx",
          "Input.jsx"
        ]
      },
      {
        "hooks": [
          "useValue.js"
        ]
      },
      "index.js"
    ]
  },
  {
    "public": [
      "logo.svg"
    ]
  },
  "README.md"
]
<SortableTree
    items={ tree }
    onChange={ ( next , change ) => setTree( next ) }
/>
// change = { item , fromParent , toParent , fromIndex , toIndex }

Async Change with API Call (optimistic revert on failure)

src
components
Button.jsx
Input.jsx
hooks
useValue.js
index.js
public
logo.svg
README.md
<SortableTree
    defaultItems={ tree }
    onChange={ ( next , change ) => api.save( next ) }
/>
// onChange may return a promise : a rejection restores the previous tree

Max Depth (maxDepth = 2) + drop indicator

Nesting is capped at 2 levels : dragging a row deeper snaps back to the limit. The blue line is the drop indicator — it shows the exact insertion point and, by its indentation, the depth the node will land at. A folder counts its own subtree against the limit.

src
components
Button.jsx
Input.jsx
hooks
useValue.js
index.js
public
logo.svg
README.md
<SortableTree maxDepth={ 2 } defaultItems={ tree } renderNode={ ... } />

Flat List (maxDepth = 0 — a single root level, no nesting)

With maxDepth={ 0 } every node stays at depth 0 : dragging a row sideways never indents it, so the tree behaves as a plain sortable list — only the vertical order changes. collapsible={ false } is paired with it since there is no subtree left to fold.

Introduction
Installation
Usage
API Reference
FAQ
<SortableTree
    maxDepth={ 0 }
    collapsible={ false }
    defaultItems={ [ { id , label } , ... ] }
    renderNode={ node => <SortableTreeItem>{ node.label }</SortableTreeItem> }
/>

Nesting Rule (canNest — only folders accept children)

Files can be reordered but never receive children : dragging a node under a file walks the drop up to the file's own level (its parent folder). The drop indicator turns red when no valid parent exists at a position.

src
app
page.jsx
utils.js
README.md
<SortableTree
    canNest={ ( item , parent ) => !parent || parent.type === 'folder' }
    defaultItems={ tree }
    renderNode={ ... }
/>

Expand All / Collapse All (controlled collapse)

The collapsed state is controlled via collapsed + onCollapsedChange, so the parent can drive it — here two buttons set it to every folder id (collapse all) or to an empty list (expand all).

src
components
Button.jsx
Input.jsx
hooks
useValue.js
index.js
public
logo.svg
README.md
<SortableTree
    collapsed={ collapsedIds }
    onCollapsedChange={ setCollapsedIds }
    defaultItems={ tree } renderNode={ ... }
/>

Frozen Tree (disabled + collapsible = false)

disabled stops all dragging and collapsible={ false } removes the chevrons and keeps every node expanded — a read-only, fully static tree.

src
components
Button.jsx
Input.jsx
hooks
useValue.js
index.js
public
logo.svg
README.md
<SortableTree disabled collapsible={ false } defaultItems={ tree } renderNode={ ... } />

Add / Remove Nodes Dynamically (insert into a subfolder)

The tree is a controlled nested value, so add / remove are plain state updates. Each folder has a « + » button that inserts a child into that folder's children (targeted by its id) — the folder auto-expands to reveal it ; every node has a « × » to delete it (with its subtree). Reordering by drag still works alongside.

src
components
Button.jsx
index.js
README.md
import insertNode from 'oihana-next-ui/helpers/trees/insertNode' ;
import removeNode from 'oihana-next-ui/helpers/trees/removeNode' ;
// add into a subfolder (by id) — or null for the top level
setTree( t => insertNode( t , folderId , newNode ) ) ;
// remove a node and its subtree
setTree( t => removeNode( t , nodeId ) ) ;

Props Reference

PropTypeDescription
canNestfunction(draggedItem, parentItem | null) => boolean — null = top level ; a rejected parent makes the drop walk up to the nearest valid ancestor (red indicator if none)
collapsedArrayControlled list of collapsed node ids ; pair with onCollapsedChange
collapsiblebooleanAllow expanding/collapsing (default true) ; false hides the chevrons and keeps every node expanded
defaultCollapsedArrayIds of nodes collapsed initially (uncontrolled collapse)
defaultItemsArrayUncontrolled initial tree (the component owns and updates it)
disabledbooleanDisable dragging for every node (default false) ; the tree can still be expanded/collapsed
getItemIdfunction(item) => string | number — unique id accessor (defaults to item.id)
handlebooleanShow a drag handle on each row (default true) ; false makes the whole row draggable
indentnumberIndentation width per depth level, in pixels (default 24)
itemsArrayControlled nested tree [ { id , children : [...] } ] ; pair with onChange
maxDepthnumberMaximum nesting depth ; a dragged folder counts its own subtree height against it
onChangefunction(tree, { item, fromParent, toParent, fromIndex, toIndex }) => void | Promise — a rejected promise reverts the tree (uncontrolled)
onCollapsedChangefunction(collapsedIds) => void — called with the new collapsed ids on each toggle
renderNodefunction(item, { depth, collapsed, childCount }) => a SortableTreeItem element (required)