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

dnd-tree-sortable

v2.0.0

Published

Accessible React sortable trees with folder explorer behavior, virtualization, and lazy children

Readme

Sortable Tree Registry

A shadcn registry for a React drag-and-drop tree using ScrollArea and Tailwind CSS. Install the source into your project, customize the rows, and own the component.

  • Nested sorting with pointer and keyboard controls
  • Native scrolling through shadcn ScrollArea
  • Collapsible and removable nodes
  • Controlled state for saving changes
  • Custom row content and controls
  • Tailwind styling with shadcn theme tokens, Button variants, and Badge

Install from the registry

Use a React 18.3 or React 19 project initialized with shadcn/ui, with Tailwind CSS 4 and shadcn theme tokens configured. Install from the hosted registry:

pnpm dlx shadcn@latest add https://ductinhkzz.github.io/dnd-tree-sortable/r/sortable-tree.json

The CLI installs dependencies, adds shadcn scroll-area, button, and badge as registry dependencies, and copies the tree into your configured components directory under dnd-tree-sortable. The manifest uses @components/ targets, so both project-root and src/components layouts work. Imports use your configured aliases; the example below assumes @/components. Existing shadcn primitives are reused; review any CLI overwrite prompt if you have customized those files.

You can also configure a namespace in your consumer project's components.json:

{
  "registries": {
    "@dnd-tree": "https://ductinhkzz.github.io/dnd-tree-sortable/r/{name}.json"
  }
}

Then run pnpm dlx shadcn@latest add @dnd-tree/sortable-tree. This is a user-configured namespace; it is not registered in shadcn's public registry index.

Usage

'use client';

import { useState } from 'react';
import { SortableTree, type TreeItem } from '@/components/dnd-tree-sortable';

export function TreeDemo() {
  const [items, setItems] = useState<TreeItem[]>([
    {
      id: 'Pages',
      children: [
        { id: 'Introduction', children: [] },
        { id: 'Installation', children: [] },
      ],
    },
    { id: 'Components', children: [] },
  ]);

  return (
    <SortableTree items={items} onItemsChange={setItems} height={400} indentationWidth={24} collapsible removable />
  );
}

Every item must have a unique string or numeric id and a children array. Extra fields are retained when items move. Use TreeItem<YourData> and components.ItemContent to render custom data. Keep IDs and structure stable during a drag.

For uncontrolled use, pass defaultItems instead of items. defaultItems is only read on mount. onItemsChange reports the entire tree after sorting, collapsing, removing, or loading children. When using items, update it in onItemsChange.

Props

| Prop | Purpose | Default | | ------------------- | ------------------------------------------------------------- | ----------------- | | loadChildren | Async child loader; receives the folder and an AbortSignal | — | | mode | reorder, tree, or explorer movement rules | tree | | items | Controlled tree data | — | | defaultItems | Initial uncontrolled tree | [] | | onItemsChange | Receives the updated full tree | — | | height | Scroll viewport height | 400 | | className | Override the ScrollArea container with Tailwind classes | — | | aria-label | Accessible name for the sortable list | Sortable items | | emptyMessage | Content shown when no items remain | No items yet. | | indentationWidth | Pixels per nesting level | 24 | | collapsible | Show collapse controls | false | | removable | Show removal controls | false | | indicator | Show a drop marker in reorder/tree modes | false | | components | Replace Handle, Collapse, Remove, ItemContent, or Badge | Built-in controls | | renderItemContent | Replace the whole row; forward supplied refs and handle props | Built-in row | | onChange | Legacy callback with the moved item's before/after position | — | | onRemove | Receives the removed ID | — |

Choose a movement mode for your use case:

| Mode | Behavior | | ---------------- | ------------------------------------------------------------------------------------------------- | | reorder | Reorder siblings; the parent stays fixed. | | tree (default) | Reorder and change nesting, including ancestor moves. | | explorer | Move files/folders into a different folder or workspace root; existing sibling order stays fixed. |

Focus a drag handle and press Space or Enter to pick up. Up/Down chooses a target, Space/Enter drops, and Escape cancels. Left/Right changes nesting in tree mode; horizontal arrows have no effect in reorder mode. Keyboard targets scroll into view.

The component uses semantic utilities such as bg-background, text-foreground, bg-accent, and ring-ring. It inherits the consumer's light/dark theme, border radius, and color tokens. Controls have visible keyboard focus, item-specific labels, expanded state, and decorative icons hidden from assistive technology. Reorder and hierarchy use a labeled list; Explorer uses tree/treeitem roles, hierarchy metadata, and a single row tab stop with arrow-key navigation. Action uses the native button with shadcn buttonVariants so handle refs work on both React 18 and React 19.

Virtuoso and CSS Modules have been removed. Large trees now use TanStack Virtual inside the shadcn ScrollArea; small trees render all expanded nodes. The npm stylesheet compiles the same Tailwind classes without adding a global preflight reset. Registry consumers use their existing Tailwind pipeline and do not import that stylesheet.

Explorer and large trees

Use mode="explorer" for folder/file icons, single selection, click-to-toggle folders, arrow-key navigation, Home/End, typeahead, and Enter/double-click file activation. Supply kind: "folder" for empty folders and kind: "file" for leaves. Nodes without kind are inferred as folders when they have children. Files cannot receive children during a drag. Folders expand after a 700 ms drag hover; cancelling the drag keeps that expansion.

<SortableTree
  mode='explorer'
  defaultItems={[
    { id: 'src', kind: 'folder', children: [{ id: 'main', label: 'main.tsx', kind: 'file', children: [] }] },
    { id: 'empty', kind: 'folder', children: [] },
  ]}
  onSelect={(item) => console.log('Select', item.id)}
  onOpen={(file) => console.log('Open', file.id)}
  virtualize={{ threshold: 200, estimateSize: 36, overscan: 8 }}
/>

selectedId controls selection; update it from onSelect. items/onItemsChange continue to control tree structure. Opening a file calls onOpen; the host application supplies its editor or preview. Rename/create and multi-selection are outside this first Explorer implementation. Folder rows use a borderless Explorer layout, colored file-type icons, and a rounded selection highlight. Drag the row to move it; Alt+M reveals and focuses the keyboard move handle. Space/Enter picks up, Up/Down chooses a folder, and Left chooses workspace root. Row arrow keys navigate the Explorer. Folder drops append to the destination and expand it; dropping into the current parent does nothing. Files cannot receive children and folders cannot move into their own descendants.

Virtualization activates automatically at 200 visible nodes in all modes, so collapsed descendants do not count. Set virtualize={true} to always virtualize or virtualize={false} to render every visible row. Rows are measured for custom heights. Focused rows and drag sources stay mounted; keyboard targets are revealed before the drag moves. Custom renderItemContent implementations must forward wrapperRef, wrapperStyle, data-index, and the supplied accessibility/event props to the outer row (the built-in row does this). The playground includes Reorder, Hierarchy, Folders, and 1,000-file examples.

Dynamic child loading

Mark a folder with childrenLoaded: false, start it collapsed, and provide loadChildren. Only expanded, visible folders request children. Unmarked items retain their existing behavior.

import { SortableTree, type LoadChildren } from '@/components/dnd-tree-sortable';

const loadChildren: LoadChildren = async (folder, { signal }) => {
  const response = await fetch(`/api/tree/${encodeURIComponent(String(folder.id))}`, { signal });
  if (!response.ok) throw new Error('Could not load children');
  return response.json(); // TreeItem[] with globally unique IDs
};

<SortableTree
  mode='explorer'
  defaultItems={[{ id: 'src', kind: 'folder', collapsed: true, childrenLoaded: false, children: [] }]}
  loadChildren={loadChildren}
/>;

The loader returns the folder's immediate children. Return more collapsed folders with childrenLoaded: false to load deeper levels on demand. A visible folder that starts expanded loads automatically; loading a parent does not fetch its collapsed descendants.

Successful responses set childrenLoaded: true in the tree, including empty results. Pending requests are deduplicated and re-expansion uses the cached children. Rows show a spinner and aria-busy while loading; failures keep the tree intact and expose a keyboard-accessible retry button. A failed request does not retry automatically.

Collapsing a folder keeps its request running and caches the response without reopening it. Removing the folder, unmounting the tree, or removing loadChildren aborts its request; late responses are discarded even if the loader ignores the signal. Responses merge with the latest tree so moves, renames, and other loads are preserved. Files moved into a loading folder remain there. Duplicate IDs are treated as a load failure.

Controlled trees must apply onItemsChange to retain loaded children. To refetch a completed folder, set its childrenLoaded back to false (also clear children if the remote result should replace cached contents). Use a new component key when replacing the entire data source, which also cancels outstanding requests. Virtualization automatically handles large loaded results. The Dynamic loading playground starts with four folders and includes nested loading, 1,000 fetched files, an empty result, and a retry example.

Develop and build

Use Node.js 24 LTS (see .nvmrc) and pnpm 12.10.1 (pinned in package.json). Node.js 22.13+ is also supported. From the repository root:

pnpm install --frozen-lockfile
pnpm run dev

pnpm dev, pnpm test, and pnpm test:watch rebuild the registry before starting. The pnpm lockfile replaces package-lock.json; CI installs with --frozen-lockfile.

Visit http://localhost:5173/dnd-tree-sortable/. The live homepage includes preview/code tabs, theme switching, collapse/removal controls, and a copyable install command that uses the current origin.

pnpm run format:check
pnpm run lint
pnpm run typecheck
pnpm run build-docs
pnpm test
pnpm run build

registry.json is the catalog and source manifest. It reads the canonical implementation in lib/; there is no second copy to maintain. Add every new source file to its files array with an @components/dnd-tree-sortable/ target. Registry coverage tests catch omitted files. The local shadcn primitive files in lib/components/ui and npm-only package.ts, styles.css, and theme.css are excluded from the registry; registryDependencies installs the primitives in consumer projects.

shadcn build generates docs/public/r/registry.json and docs/public/r/sortable-tree.json; these are generated artifacts and are not committed. build-docs rebuilds the registry before compiling the website so docs/dist includes both the website and registry JSON.

Test from a disposable, initialized consumer project:

pnpm dlx shadcn@latest add http://localhost:5173/dnd-tree-sortable/r/sortable-tree.json

CI checks formatting, lint, file naming, types, the latest commit message, website/registry build, Vitest tests, the library build, and production dependency advisories. The existing GitHub Pages workflow deploys docs/dist after merging to master, using GitHub's official Pages artifact deployment with scoped Pages/OIDC permissions. The registry is available publicly only after that deployment succeeds.

Install from npm

Install the npm library:

pnpm add dnd-tree-sortable@^2 react react-dom

Import the component and its stylesheet:

import { SortableTree } from 'dnd-tree-sortable';
import 'dnd-tree-sortable/dist/assets/style.css';

The registry flow copies Tailwind-styled source and installs shadcn primitives. The npm flow bundles the local primitive source and provides precompiled CSS; existing consumers keep the stylesheet import above. The release workflow validates the source, packed consumers, and matching version tag before publishing. Packing or building locally does not publish.

Credits

Built on the dnd-kit sortable tree example with shadcn ScrollArea. Registry format follows the shadcn registry guide.

Tooling and naming

  • Oxlint checks TypeScript, React hooks, accessibility, and Vitest code. Vite 8 uses the Oxc transform pipeline.
  • Oxfmt replaces Prettier. Run pnpm run format to write changes, or pnpm run format:check to verify them.
  • Vitest replaces the Node/tsx test runner. pnpm test rebuilds the registry and runs utility, registry, SSR, and React interaction tests; pnpm run test:watch watches tests.
  • commitlint enforces Conventional Commits through Husky's commit-msg hook. Use messages such as feat: add custom tree rows or fix: preserve numeric item ids. CI checks the newest commit; existing repository history is not rewritten.
  • The pre-commit hook checks formatting, lint, and types. pnpm run check runs all project checks.
  • File and folder names use kebab-case, enforced by scripts/check-file-names.mjs. Standard dotfiles/config extensions and the conventional README.md, CHANGELOG.md, CONTRIBUTING.md, and LICENSE names are allowed. Component/export names remain PascalCase.

Dependency review — October 10, 2026

Direct versions were checked against npm's stable latest tags. The lockfile records the tested versions.

| Area | Version | Review | | ------------------------------------------ | --------------- | --------------------------------------------------------------------------- | | React / React DOM | 19.3.0 | Site and interaction tests migrated from React 18 | | Vite / React plugin | 8.3.4 / 6.1.2 | Oxc/Rolldown build; removed old Rollup plugin configuration | | TypeScript | 7.0.2 | Removed baseUrl; direct declaration generation replaces vite-plugin-dts | | Vitest | 5.0.3 | Node utility/SSR tests and jsdom interaction tests | | Oxlint / Oxfmt | 1.87.0 / 0.72.0 | Replaces ESLint and Prettier | | commitlint / Husky | 21.2.3 / 9.1.7 | Conventional Commit validation and local hooks | | Tailwind CSS / Vite plugin | 4.3.3 | Replaces CSS Modules with semantic theme utilities | | Radix UI | 1.7.0 | Official shadcn ScrollArea, Button, and Badge foundations | | Lucide / cn | 1.54.0 / 0.4.0 | Theme-aware icons and the class merge helper used by current shadcn source | | dnd-kit React / DOM / abstract / collision | 0.5.0 | New architecture; removed core, sortable, and utilities packages | | TanStack Virtual | 3.14.14 | Headless virtualization within the shadcn ScrollArea | | shadcn | 4.21.4 | Latest stable registry CLI |

Removed react-virtuoso, rc-scrollbars (its peer range stops at React 18), ESLint and its plugins, Prettier, tsx, glob, the Rollup visualizer, and vite-plugin-dts. The unused React Pages/MDX docs tree, old docs lockfile, template pages, and redundant TypeScript configs were removed. Runtime dependencies now live in dependencies, and the npm build externalizes them rather than copying them into the bundle.

Migrated to the new dnd-kit React API: DragDropProvider, @dnd-kit/react/sortable, ref-based handles, and the new DOM sensors/accessibility plugin. Rows explicitly disable optimistic DOM sorting so React owns row order. Up/down keys follow visible tree order; left/right keys change nesting; Escape cancels. Pointer collision detection uses pointer distance to visible rows. Subtrees and custom payloads are preserved on drop.

Custom components.Handle implementations receive elementRef in ActionProps; attach it to the handle button (<button ref={elementRef}>) or pass all props through the exported Action component. Custom renderItemContent implementations must forward ref, wrapperRef, and handleProps.elementRef to their source, target wrapper, and handle. The obsolete classic drop animation was removed; the overlay ends immediately on drop. Registry installs use only the new dnd-kit architecture.

Logic and UX review

  • Moves run through a pure moveTreeItem helper. Stale IDs, hidden descendant targets, invalid offsets/indentation, and unchanged drops are ignored safely.
  • The legacy positional callback now receives recomputed depth and sibling index for the moved node.
  • Nesting into a collapsed parent expands it after a successful drop so the result stays visible.
  • Flattening uses one accumulator and descendant exclusion uses a Set, avoiding repeated array copies and linear membership scans.
  • Drag placeholders retain their row height. Optional before/after markers show the projected nesting level without shrinking the list.
  • Collapse/removal controls are disabled during a drag; keyboard Escape cancels the move without applying a reorder. Hover-expanded Explorer folders remain open.
  • Tests cover tree invariants across reorder/nesting combinations, real ScrollArea and sensor interactions, numeric IDs, custom payloads/handles, controlled updates, cancellation, empty states, and SSR.

pnpm audit --prod reports zero production vulnerabilities. The full pnpm audit reports one high-severity development-only advisory for braces, reached through three shadcn dependency paths (fast-glob, ts-morph, and @shadcn/registry). This is the same advisory previously reported as seven package-level entries by npm. The installed latest shadcn version still carries them; no forced downgrade or unverified override was applied. Track GHSA-vfj7-8cjw-p6xm and update the CLI when a safe upstream fix is available.

Migrating from 1.x

The rework targets 2.0.0 because custom rendering and drag integration changed. Keep the public npm import and explicit stylesheet import; reinstall from the registry if you own the copied source.

  • Replace classic dnd-kit integrations with the new React/DOM API. Forward handleProps.elementRef or custom ActionProps.elementRef to the handle button. Custom rows must forward the source ref, target wrapperRef, wrapperStyle, data-index, and accessibility/event props.
  • Update CSS Modules overrides to Tailwind classes/shadcn tokens. Registry projects require Tailwind 4; npm projects use the supplied compiled stylesheet and do not need Tailwind.
  • Replace Virtuoso/rc-scrollbars integrations with virtualize and the built-in ScrollArea. No Virtuoso instance is exposed.
  • Use controlled items with onItemsChange for persistence. The positional onChange callback remains available but does not report collapse, removal, or lazy loading.
  • Prefer public exports. Internal filenames use kebab-case and deep npm paths are not exported. The npm package is ESM.

Contributing and releases

See CONTRIBUTING.md for setup, source conventions, consumer checks, and the release process. CHANGELOG.md tracks the upcoming major release and subsequent changes. Both npm and registry distributions include the MIT license.

Before releasing, run pnpm check, pnpm check:package, and pnpm audit:production. The registry is self-hosted: deploy docs/dist and verify its public installation URL. Registering a namespace in shadcn's public index is optional and separate from serving the registry.