npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nosferatu500/react-sortable-tree

v5.0.0

Published

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

Readme

React Sortable Tree

NPM version NPM license NPM total downloads NPM monthly downloads PRs 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'

const initialData: TreeItem[] = [
  { title: 'Chicken', children: [{ title: 'Egg' }] },
  { title: 'Fish', children: [{ title: 'Fingerling' }] },
]

export function ExampleTree() {
  const [treeData, setTreeData] = useState(initialData)

  return (
    <div style={{ height: 400 }}>
      <SortableTree
        treeData={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

| Prop | Type | Description | |------|------|-------------| | treeData | TreeItem[] | Array of tree nodes with { title?, subtitle?, expanded?, children?, ...custom } | | onChange | (treeData: TreeItem[]) => void | Called on every tree data change |

Appearance & layout

| Prop | Type | Default | Description | |------|------|---------|-------------| | rowHeight | number \| ((treeIndex, node, path) => number) | 62 | Height of each row in pixels | | rowDirection | 'ltr' \| 'rtl' | 'ltr' | Layout direction | | scaffoldBlockPxWidth | number | 44 | Width of indent per level | | slideRegionSize | number | 100 | Size of the drag slide region | | style | CSSProperties | - | Styles for the outer container | | innerStyle | CSSProperties | - | Styles for the virtual list | | className | string | - | Class name for the outer container |

Theming & custom renderers

| Prop | Type | Description | |------|------|-------------| | theme | ThemeProps | Theme object (see Theming section) | | nodeContentRenderer | ComponentType | Custom component for node content | | treeNodeRenderer | ComponentType | Custom component for the entire tree row | | placeholderRenderer | ComponentType | Custom component for empty tree state |

Drag & drop

| Prop | Type | Default | Description | |------|------|---------|-------------| | canDrag | boolean \| ((params) => boolean) | true | Whether nodes can be dragged | | canDrop | (params) => boolean | - | Validate if a drop is allowed | | canNodeHaveChildren | (node) => boolean | () => true | Whether a node can have children | | maxDepth | number | - | Maximum nesting depth | | shouldCopyOnOutsideDrop | boolean \| ((params) => boolean) | false | Copy node when dropped outside | | dndType | string | - | 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

| Prop | Type | Description | |------|------|-------------| | searchQuery | string | Search query string | | searchMethod | (params) => boolean | Custom search matching function | | searchFocusOffset | number | Index of the focused match | | searchFinishCallback | (matches) => void | Called when search completes | | onlyExpandSearchedNodes | boolean | Collapse non-matching paths |

Other

| Prop | Type | Description | |------|------|-------------| | generateNodeProps | (params) => object | Add custom props to each node | | getNodeKey | (node) => string \| number | Generate stable node keys | | onVisibilityToggle | (params) => void | Called when node expands/collapses | | loadCollapsedLazyChildren | boolean | Load lazy children before expanding | | virtuaRef | RefObject<VListHandle> | Direct access to the virtual list | | dragDropManager | object | External 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:

type ThemeProps = {
  style?: React.CSSProperties
  innerStyle?: React.CSSProperties
  scaffoldBlockPxWidth?: number
  slideRegionSize?: number
  treeNodeRenderer?: React.ComponentType
  nodeContentRenderer?: React.ComponentType
  placeholderRenderer?: React.ComponentType
  dndType?: 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'

function FileTree() {
  const [treeData, setTreeData] = useState([
    { title: 'src', isDirectory: true, expanded: true, children: [
      { title: 'index.ts' },
      { title: 'App.tsx' },
    ]},
    { title: 'package.json' },
  ])

  return (
    <div className={FILE_EXPLORER_THEME_CLASS}>
      <SortableTree
        treeData={treeData}
        onChange={setTreeData}
        theme={fileExplorerTheme}
        rowHeight={28}
        // Only folders can have children
        canNodeHaveChildren={(node) => node.isDirectory === true}
        // Only allow dropping into folders
        canDrop={({ 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:
export const myTheme = {
  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