Installation
Install the package along with its peer dependencies:
npm install dnd-block-tree @dnd-kit/core @dnd-kit/utilitiesRequires React 18+ and dnd-kit/core 6+
Basic Usage
Define your block type, create renderers, and render the tree:
import { BlockTree, type BaseBlock, type BlockRenderers } from 'dnd-block-tree'
import { useState } from 'react'
// Define your block type
interface MyBlock extends BaseBlock {
type: 'section' | 'task' | 'note'
title: string
}
// Create renderers for each type
const renderers: BlockRenderers<MyBlock, ['section']> = {
section: ({ block, children, isExpanded, onToggleExpand }) => (
<div className="border rounded p-2">
<button onClick={onToggleExpand}>
{isExpanded ? '▼' : '▶'} {block.title}
</button>
{isExpanded && <div className="ml-4">{children}</div>}
</div>
),
task: ({ block }) => <div className="p-2">{block.title}</div>,
note: ({ block }) => <div className="p-2 italic">{block.title}</div>,
}
function App() {
const [blocks, setBlocks] = useState<MyBlock[]>([
{ id: '1', type: 'section', title: 'Tasks', parentId: null, order: 0 },
{ id: '2', type: 'task', title: 'Do something', parentId: '1', order: 0 },
])
return (
<BlockTree
blocks={blocks}
renderers={renderers}
containerTypes={['section']}
onChange={setBlocks}
/>
)
}Key Concepts
BaseBlock - All blocks must have id, type, parentId, and order.
containerTypes - Block types that can have children. These receive extra props like children and isExpanded.
renderers - A map of block type to render function. TypeScript ensures correct props for containers vs leaves.
Callbacks & Events
Hook into the drag-and-drop lifecycle for real-time sync, analytics, or custom behavior:
<BlockTree
blocks={blocks}
renderers={renderers}
containerTypes={['section']}
onChange={setBlocks}
// Drag lifecycle callbacks
onDragStart={(e) => {
console.log('Started dragging:', e.block)
// Return false to prevent drag
}}
onDragMove={(e) => {
console.log('Dragging over:', e.overZone)
}}
onDragEnd={(e) => {
console.log('Dropped at:', e.targetZone)
if (!e.cancelled) {
// Sync with server, analytics, etc.
}
}}
onDragCancel={(e) => {
console.log('Drag cancelled')
}}
// Block movement callback
onBlockMove={(e) => {
console.log('Block moved from:', e.from, 'to:', e.to)
// Great for real-time sync
}}
// UI state callbacks
onExpandChange={(e) => {
console.log('Container expanded:', e.expanded)
}}
onHoverChange={(e) => {
console.log('Hovering zone:', e.zoneType)
}}
/>onDragStart
Called when drag begins. Return
false to prevent.onDragMove
Called during drag (debounced). Includes coordinates and hover zone.
onBlockMove
Called after successful drop with from/to positions. Great for server sync.
onHoverChange
Called when hovering different drop zones. Useful for custom indicators.
Customization
Control drag behavior, drop rules, sensors, and visual feedback:
<BlockTree
blocks={blocks}
renderers={renderers}
containerTypes={['section']}
onChange={setBlocks}
// Filter which blocks can be dragged
canDrag={(block) => !block.locked}
// Filter valid drop targets
canDrop={(draggedBlock, targetZone, targetBlock) => {
// Prevent dropping sections into tasks
if (draggedBlock.type === 'section' && targetBlock?.type === 'task') {
return false
}
return true
}}
// Custom collision detection algorithm
collisionDetection={customCollisionFn}
// Sensor configuration
sensors={{
activationDistance: 10, // Pixels before drag starts
activationDelay: 200, // OR use delay instead
tolerance: 5, // Movement tolerance during delay
}}
// Initial expand state
initialExpanded="all" // 'all' | 'none' | string[]
// Live preview during drag (default: true)
showDropPreview={true}
/>Customization Options
canDragFilter which blocks can be dragged. Receives the block, returns boolean.
canDropFilter valid drop targets. Receives dragged block, zone ID, and target block.
collisionDetectionCustom collision algorithm. Default uses depth-aware detection with hysteresis that prefers nested zones at indented cursor positions.
showDropPreviewShow a ghost preview where the block will land. Uses stable zones that don't shift during drag.
Type Definitions
Full TypeScript support with comprehensive type definitions:
// Base block interface - extend for your types
interface BaseBlock {
id: string
type: string
parentId: string | null
order: number
}
// Event types
interface DragStartEvent<T> {
block: T
blockId: string
}
interface DragMoveEvent<T> {
block: T
blockId: string
overZone: string | null
coordinates: { x: number; y: number }
}
interface DragEndEvent<T> {
block: T
blockId: string
targetZone: string | null
cancelled: boolean
}
interface BlockMoveEvent<T> {
block: T
from: { parentId: string | null; index: number }
to: { parentId: string | null; index: number }
blocks: T[] // All blocks after the move
}
// Renderer props
interface BlockRendererProps<T> {
block: T
isDragging?: boolean
depth: number
}
interface ContainerRendererProps<T> extends BlockRendererProps<T> {
children: ReactNode
isExpanded: boolean
onToggleExpand: () => void
}All Exports
Everything exported from the package for building custom implementations:
// Components
export { BlockTree } from './components/BlockTree'
export { TreeRenderer } from './components/TreeRenderer'
export { DropZone } from './components/DropZone'
export { DragOverlay } from './components/DragOverlay'
// Hooks (for building custom implementations)
export { createBlockState } from './hooks/useBlockState'
export { createTreeState } from './hooks/useTreeState'
// Collision detection
export { weightedVerticalCollision, closestCenterCollision } from './core/collision'
// Sensors
export { useConfiguredSensors, getSensorConfig } from './core/sensors'
// Utilities
export {
cloneMap,
cloneParentMap,
computeNormalizedIndex,
buildOrderedBlocks,
reparentBlockIndex,
getDescendantIds,
deleteBlockAndDescendants,
} from './utils/blocks'
export { extractUUID, debounce, generateId } from './utils/helper'
// Types (see Types section for details)
export type { BaseBlock, BlockRenderers, ... } from './core/types'