Skip to content

Repository files navigation

React Sortable Tree

NPM versionNPM licenseNPM total downloadsNPM monthly downloadsPRs Welcome

Drag-and-drop sortable representation of hierarchical data for React 18/19 with virtualized rendering powered by virtua and react-dnd. Storybook demos cover both basic and advanced scenarios.

Getting started

Install the package together with its peer dependencies:

npm install @nosferatu500/react-sortable-tree react-dnd react-dnd-html5-backend
# or
yarn add @nosferatu500/react-sortable-tree react-dnd react-dnd-html5-backend

The bundle is ESM-only and includes all styles via runtime injection (no separate CSS file is required).

Quick start

import{useState}from'react'import{SortableTree,TreeItem}from'@nosferatu500/react-sortable-tree'constinitialData: TreeItem[]=[{title: 'Chicken',children: [{title: 'Egg'}]},{title: 'Fish',children: [{title: 'Fingerling'}]},]exportfunctionExampleTree(){const[treeData,setTreeData]=useState(initialData)return(<divstyle={{height: 400}}><SortableTreetreeData={treeData}onChange={setTreeData}/></div>)}

Already have a surrounding react-dnd context? Use the context-less export instead:

import{SortableTreeWithoutDndContext}from'@nosferatu500/react-sortable-tree'

Component props

All props are typed in ReactSortableTreeProps (see src/react-sortable-tree.tsx).

Required props

PropTypeDescription
treeDataTreeItem[]Array of tree nodes with { title?, subtitle?, expanded?, children?, ...custom }
onChange(treeData: TreeItem[]) => voidCalled on every tree data change

Appearance & layout

PropTypeDefaultDescription
rowHeightnumber | ((treeIndex, node, path) => number)62Height of each row in pixels
rowDirection'ltr' | 'rtl''ltr'Layout direction
scaffoldBlockPxWidthnumber44Width of indent per level
slideRegionSizenumber100Size of the drag slide region
styleCSSProperties-Styles for the outer container
innerStyleCSSProperties-Styles for the virtual list
classNamestring-Class name for the outer container

Theming & custom renderers

PropTypeDescription
themeThemePropsTheme object (see Theming section)
nodeContentRendererComponentTypeCustom component for node content
treeNodeRendererComponentTypeCustom component for the entire tree row
placeholderRendererComponentTypeCustom component for empty tree state

Drag & drop

PropTypeDefaultDescription
canDragboolean | ((params) => boolean)trueWhether nodes can be dragged
canDrop(params) => boolean-Validate if a drop is allowed
canNodeHaveChildren(node) => boolean() => trueWhether a node can have children
maxDepthnumber-Maximum nesting depth
shouldCopyOnOutsideDropboolean | ((params) => boolean)falseCopy node when dropped outside
dndTypestring-Custom drag type for multi-tree setups
onMoveNode(params) => void-Called after a node is moved
onDragStateChanged(params) => void-Called when drag state changes

Search

PropTypeDescription
searchQuerystringSearch query string
searchMethod(params) => booleanCustom search matching function
searchFocusOffsetnumberIndex of the focused match
searchFinishCallback(matches) => voidCalled when search completes
onlyExpandSearchedNodesbooleanCollapse non-matching paths

Other

PropTypeDescription
generateNodeProps(params) => objectAdd custom props to each node
getNodeKey(node) => string | numberGenerate stable node keys
onVisibilityToggle(params) => voidCalled when node expands/collapses
loadCollapsedLazyChildrenbooleanLoad lazy children before expanding
virtuaRefRefObject<VListHandle>Direct access to the virtual list
dragDropManagerobjectExternal react-dnd manager

Theming

The component supports theming through CSS variables, the theme prop, and custom renderers.

CSS Variables

Override these CSS variables on the .rst__tree class or a parent element:

.my-custom-theme .rst__tree {
--rst-row-height:62px;
--rst-block-width:44px;
--rst-handle-width:44px;
--rst-line-color:#000;
--rst-line-highlight:#36c2f6;
--rst-line-highlight-arrow: white;
--rst-primary-color:#36c2f6;
--rst-focus-color:#fc6421;
--rst-match-color:#0080ff;
--rst-bg-landing: lightblue;
--rst-bg-cancel:#e6a8ad;
--rst-text-color:#333;
--rst-icon-color:#6DB3F2;
--rst-button-bg:#fff;
--rst-button-border:#989898;
}

Theme prop

The theme prop accepts an object with these properties:

typeThemeProps={style?: React.CSSPropertiesinnerStyle?: React.CSSPropertiesscaffoldBlockPxWidth?: numberslideRegionSize?: numbertreeNodeRenderer?: React.ComponentTypenodeContentRenderer?: React.ComponentTypeplaceholderRenderer?: React.ComponentTypedndType?: string}

Theme values are merged with component props, with direct props taking precedence.

Example: File Explorer Theme

The library includes a File Explorer theme example in the Storybook demos:

import{SortableTree}from'@nosferatu500/react-sortable-tree'import{fileExplorerTheme,FILE_EXPLORER_THEME_CLASS}from'./themes/file-explorer'functionFileTree(){const[treeData,setTreeData]=useState([{title: 'src',isDirectory: true,expanded: true,children: [{title: 'index.ts'},{title: 'App.tsx'},]},{title: 'package.json'},])return(<divclassName={FILE_EXPLORER_THEME_CLASS}><SortableTreetreeData={treeData}onChange={setTreeData}theme={fileExplorerTheme}rowHeight={28}// Only folders can have childrencanNodeHaveChildren={(node)=>node.isDirectory===true}// Only allow dropping into folderscanDrop={({ nextParent })=>!nextParent||nextParent.isDirectory===true}/></div>)}

For dark mode, add the rst__file-explorer-dark class to the wrapper.

Creating custom themes

To create a custom theme:

  1. Create a custom nodeContentRenderer component (see src/node-renderer-default.tsx for reference)
  2. Add CSS styles with your theme class
  3. Export a theme object:
exportconstmyTheme={nodeContentRenderer: MyCustomNodeRenderer,scaffoldBlockPxWidth: 24,slideRegionSize: 50,}

Data helper functions

Utilities exported from the package:

Node manipulation

  • addNodeUnderParent({ treeData, newNode, parentKey, getNodeKey, expandParent?, addAsFirstChild? }) - Add a node under a parent
  • insertNode({ treeData, newNode, depth, minimumTreeIndex, getNodeKey, expandParent? }) - Insert a node at a specific position
  • removeNode({ treeData, path, getNodeKey }) - Remove a node by path
  • removeNodeAtPath({ treeData, path, getNodeKey }) - Remove a node at exact path
  • changeNodeAtPath({ treeData, path, newNode, getNodeKey }) - Update a node at path

Tree inspection

  • getNodeAtPath({ treeData, path, getNodeKey }) - Get node at path
  • getDescendantCount({ node }) - Count all descendants
  • getDepth(node) - Get nesting depth of a node
  • isDescendant(older, younger) - Check parent-child relationship
  • getVisibleNodeCount({ treeData }) - Count visible (expanded) nodes

Tree traversal

  • walk({ treeData, getNodeKey, callback, ignoreCollapsed? }) - Walk tree depth-first
  • map({ treeData, getNodeKey, callback, ignoreCollapsed? }) - Transform all nodes
  • toggleExpandedForAll({ treeData, expanded }) - Expand or collapse all nodes
  • find({ treeData, getNodeKey, searchQuery, searchMethod, expandAllMatchPaths? }) - Search with path expansion

Data conversion

  • getFlatDataFromTree({ treeData, getNodeKey, ignoreCollapsed? }) - Convert to flat array
  • getTreeFromFlatData({ flatData, getKey, getParentKey, rootKey? }) - Convert from flat array

Default handlers

  • defaultGetNodeKey({ treeIndex }) - Default key generator (uses index)
  • defaultSearchMethod({ node, searchQuery }) - Default search (matches title)

License

MIT

About

Drag-and-drop sortable component for nested data and hierarchies

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages