@hermes-solution/baekdu-dnd
v0.1.0
Published
Lightweight, headless React tree drag-and-drop library with before/inside/after positioning. Zero CSS, full control.
Maintainers
Readme
@aims/baekdu-dnd
Lightweight, headless React tree drag-and-drop library with before/inside/after positioning. Zero CSS, full control.
✨ Features
- 🎯 Headless - Complete control over UI and styling
- 🎨 Zero CSS - Bring your own styles (Tailwind, CSS Modules, CSS-in-JS)
- 📍 Precise Positioning - Drop before/inside/after with visual indicators
- 🪶 Lightweight - No dependencies, native HTML5 Drag and Drop API
- 🎭 Fully Typed - Written in TypeScript with full type safety
- 🌲 Tree Optimized - Built specifically for hierarchical data structures
📦 Installation
npm install @aims/baekdu-dnd🚀 Quick Start
The minimal import is now just the provider, item, and collection hook:
import {TreeDndProvider, TreeItem, useTreeCollection} from '@aims/baekdu-dnd';
function FileExplorer({initialTree}) {
const {flattened, handleDrop} = useTreeCollection(initialTree);
return (
<TreeDndProvider onDrop={handleDrop}>
{flattened.map((item) => (
<TreeItem key={item.id} id={item.id} parentId={item.parentId} depth={item.depth} index={item.index} value={item.node}>
{({draggableProps, renderIndicator}) => (
<div {...draggableProps}>
{renderIndicator()}
{item.node.name}
</div>
)}
</TreeItem>
))}
</TreeDndProvider>
);
}flattened already contains depth, parentId, index, and a stable path, so you can focus on rendering only.
Forward the canDrop handler returned by the hook to TreeDndProvider if you want the indicator to disappear immediately for disallowed gaps.
renderIndicator()returns the built-in drop indicator. Omit it if you plan to render your own visuals based ondropPosition.draggablePropsalready includesref,onDragEnter, and all necessary drag handlers—just spread it onto your focusable element.
renderIndicator()returns the built-in drop indicator. Omit it if you plan to render your own visuals based ondropPosition.draggablePropsalready includesref,onDragEnter, and all necessary drag handlers—just spread it onto your focusable element.
Drop Event Payload
onDrop receives:
draggedId– the id of the item being movedtargetId– canonical drop target (after/before deduped)- When you drop after the final root,
targetIdwill be the virtual tail id (__tree-dnd-root-tail__); rely onslot.parentId(null) andslot.indexfor ordering there. dropPosition–'before','inside', or'after'slot– stable slot metadata (key,parentId,index) for orderingoriginalTargetId– raw DOM target id that fired the dropindicator– full indicator state (rect, depth,visualMode, etc.)draggedData– metadata you registered with the dragged item (viaTreeItem)targetData– metadata you registered with the canonical target
indicator.visualMode === 'subtree' means you are aiming at the gap after a branch; the default indicator now wraps the entire subtree in that case so users can see the full scope of the move. Use indicator.visualRect if you need screen-space measurements to draw custom overlays.
The provider renders a tiny, invisible tail element (
data-tree-dnd-tail="") after your tree so users can drop below the final node. Feel free to restyle it (height, margin) to match your layout.
Use slot.key plus slot.parentId to decide where in your tree to insert without worrying about the “after vs before” overlap on adjacent nodes.
Preventing Illegal Drops
Pass a canDrop guard to TreeDndProvider to veto specific slots before the user releases the mouse:
<TreeDndProvider
canDrop={({dropPosition, targetData}) => {
if (dropPosition === 'inside' && targetData?.meta?.lockChildren) {
return false; // indicator disappears immediately
}
return true;
}}
/>canDrop receives the same payload as onDrop, plus the metadata you registered via TreeItem. Returning false clears the indicator so the drop never fires.
Built-in Motion
Nodes animate into their new positions by default using a lightweight FLIP transition. To tweak or disable the behaviour, pass itemAnimations to the provider:
<TreeDndProvider itemAnimations={{ duration: 220, easing: 'cubic-bezier(.19,1,.22,1)' }}>
{/* tree */}
</TreeDndProvider>
<TreeDndProvider itemAnimations={false}>
{/* disable motion entirely */}
</TreeDndProvider>Inside the TreeItem render prop you also receive the resolved animation config, making it easy to coordinate your own CSS transitions.
Tree Utilities
Need simple path-based helpers while prototyping? The package now exports reusable utilities such as cloneTree, flattenTree, removeNodeAtPath, insertInside, insertSibling, adjustTargetPathAfterRemoval, and reorderTreeByPath, plus the shared TreePath and FlattenedTreeItem types. These are the same building blocks used by the dev playground, so you can reuse the exact reorder logic inside your app without re-implementing tree math.
📖 Core Concepts
Headless Architecture
What the library provides:
- Drag and drop state management
- Drop position calculation (before/inside/after)
- Event handling and coordination
What you control:
- All styling (CSS, Tailwind, CSS-in-JS, etc.)
- UI rendering and animations
Drop Positioning
┌─────────────────┐
│ BEFORE (25%) │ ← Drop above
├─────────────────┤
│ INSIDE (50%) │ ← Drop inside (make child)
├─────────────────┤
│ AFTER (25%) │ ← Drop below
└─────────────────┘Customize ratios:
<TreeDndProvider dropZoneRatios={{ before: 20, inside: 60, after: 20 }} />📄 License
MIT © ISO 42001 ALM Team
