solidjs-dnd
v0.0.1
Published
A SolidJS-native drag and drop library
Readme
solidjs-dnd
A SolidJS-native drag-and-drop library built on fine-grained reactivity. Primitives-based API with support for sortable lists, grids, multi-container (Kanban), and tree sorting with nesting.
Install
npm install solidjs-dnd solid-js
# or
pnpm add solidjs-dnd solid-jssolid-js ^1.9.0 is a peer dependency.
Quick start
import {
DragDropProvider,
createDraggable,
createDroppable,
type DragEvent,
} from "solidjs-dnd";
function DraggableItem() {
const draggable = createDraggable({ id: "item-1" });
return (
<div
ref={draggable.ref}
style={{
transform: draggable.isDragging()
? `translate3d(${draggable.transform().x}px, ${draggable.transform().y}px, 0)`
: undefined,
}}
>
Drag me
</div>
);
}
function DropZone() {
const droppable = createDroppable({ id: "zone-1" });
return (
<div ref={droppable.ref} classList={{ over: droppable.isOver() }}>
Drop here
</div>
);
}
function App() {
function handleDragEnd(event: DragEvent) {
if (event.over) {
console.log(`Dropped ${event.active.id} on ${event.over.id}`);
}
}
return (
<DragDropProvider onDragEnd={handleDragEnd}>
<DraggableItem />
<DropZone />
</DragDropProvider>
);
}Features
Sortable lists
Vertical, horizontal, or grid sorting with displacement-based animations. Items shift visually during drag; the array only reorders on drop.
import {
DragDropProvider,
SortableContext,
createSortable,
moveArrayItem,
closestCenter,
verticalListStrategy, // or horizontalListStrategy, createGridStrategy
type DragEvent,
} from "solidjs-dnd";
function SortableItem(props: { id: string; label: string }) {
const sortable = createSortable({ id: props.id });
const t = () => sortable.transform();
return (
<div
ref={sortable.ref}
style={{
transform: t().x || t().y
? `translate3d(${t().x}px, ${t().y}px, 0)`
: undefined,
transition: sortable.isDragging() || sortable.isSettling()
? "none"
: "transform 200ms ease",
}}
>
{props.label}
</div>
);
}
function SortableList() {
const [items, setItems] = createSignal([
{ id: "1", label: "First" },
{ id: "2", label: "Second" },
{ id: "3", label: "Third" },
]);
function handleDragEnd(event: DragEvent) {
if (!event.over) return;
const from = items().findIndex((i) => i.id === event.active.id);
const to = items().findIndex((i) => i.id === event.over!.id);
if (from !== to) setItems((prev) => moveArrayItem(prev, from, to));
}
return (
<DragDropProvider collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext ids={items().map((i) => i.id)} strategy={verticalListStrategy}>
<For each={items()}>
{(item) => <SortableItem id={item.id} label={item.label} />}
</For>
</SortableContext>
</DragDropProvider>
);
}Grid sorting
import { createGridStrategy } from "solidjs-dnd";
// Auto-detects columns from item positions
const gridStrategy = createGridStrategy();
// Or specify columns explicitly
const gridStrategy = createGridStrategy({ columns: 4 });
<SortableContext ids={ids()} strategy={gridStrategy}>
{/* grid items */}
</SortableContext>Multi-container (Kanban)
Use closestToPointer for nested droppables. Each container gets its own SortableContext with a containerId. Use transferArrayItem to move items between containers.
import {
DragDropProvider,
SortableContext,
createSortable,
createDroppable,
closestToPointer,
moveArrayItem,
transferArrayItem,
} from "solidjs-dnd";
function KanbanColumn(props: { id: string; items: Item[] }) {
const droppable = createDroppable({ id: props.id });
const ids = () => props.items.map((i) => i.id);
return (
<div ref={droppable.ref}>
<SortableContext ids={ids()} containerId={props.id}>
<For each={props.items}>
{(item) => <SortableCard item={item} />}
</For>
</SortableContext>
</div>
);
}Tree sorting
Drag to reorder and drag left/right to change nesting depth. Subtrees move as a unit.
import {
DragDropProvider,
TreeSortableContext,
createTreeSortable,
moveTreeItems,
closestCenter,
type TreeItem,
} from "solidjs-dnd";
interface Block extends TreeItem {
label: string;
}
function TreeItem(props: { item: Block }) {
const sortable = createTreeSortable({ id: props.item.id });
const t = () => sortable.transform();
return (
<div
ref={sortable.ref}
style={{
"margin-left": `${sortable.projectedDepth() * 24}px`,
transform: t().x || t().y
? `translate3d(${t().x}px, ${t().y}px, 0)`
: undefined,
transition: sortable.isDragging() || sortable.isSettling()
? "none"
: "transform 200ms ease, margin-left 200ms ease",
opacity: sortable.isInActiveSubtree() ? 0.3 : 1,
}}
>
{props.item.label}
</div>
);
}
function TreeEditor() {
const [items, setItems] = createSignal<Block[]>([
{ id: "1", depth: 0, label: "Introduction" },
{ id: "2", depth: 1, label: "Getting Started" },
{ id: "3", depth: 0, label: "Advanced" },
]);
// Capture tree context for projection data
let treeCtx;
return (
<DragDropProvider
collisionDetection={closestCenter}
onDragEnd={(event) => {
const proj = treeCtx?.projection();
if (proj) setItems((prev) => moveTreeItems(prev, event.active.id, proj));
}}
>
<TreeSortableContext items={items()} indentSize={24}>
{/* Capture context ref */}
{(() => { treeCtx = useTreeSortableContext(); return null; })()}
<For each={items()}>
{(item) => <TreeItem item={item} />}
</For>
</TreeSortableContext>
</DragDropProvider>
);
}Drag overlay
Render a floating copy of the dragged item above all content. The original item can be hidden or dimmed.
import { DragOverlay } from "solidjs-dnd";
<DragDropProvider onDragEnd={handleDragEnd}>
<MyList />
<DragOverlay dropAnimation dropAnimationDuration={200}>
{(active) => <div class="floating-card">{active.data.label}</div>}
</DragOverlay>
</DragDropProvider>Drag handles
Use activatorRef to restrict drag activation to a handle element.
const sortable = createSortable({ id: "item-1" });
<div ref={sortable.ref}>
<span ref={sortable.activatorRef} class="handle">⠁</span>
Item content
</div>Keyboard support
Add createKeyboardSensor for accessible drag-and-drop. Space/Enter to pick up and drop, arrow keys to move, Escape to cancel.
import { createPointerSensor, createKeyboardSensor } from "solidjs-dnd";
const sensors = [createPointerSensor(), createKeyboardSensor()];
<DragDropProvider sensors={sensors}>
{/* ... */}
</DragDropProvider>ARIA attributes (role, tabindex, aria-grabbed, aria-roledescription, aria-describedby) are applied automatically. A live region announces drag events to screen readers.
Axis locking
Constrain dragging to a single axis.
<DragDropProvider lockAxis="y">
{/* Only vertical dragging */}
</DragDropProvider>Touch delay
Distinguish drag from scroll on touch devices.
const sensors = [
createPointerSensor({ touchDelay: 200 }),
createKeyboardSensor(),
];If the pointer moves beyond the activation distance before the delay elapses, the gesture is treated as a scroll, not a drag.
API reference
Provider
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| collisionDetection | CollisionDetector | closestCenter | Collision algorithm |
| onDragStart | DragEventHandler | — | Called when drag starts |
| onDragOver | DragEventHandler | — | Called when over target changes |
| onDragEnd | DragEventHandler | — | Called when drag ends |
| sensors | SensorFactory[] | [createPointerSensor()] | Input sensors |
| lockAxis | "x" \| "y" | — | Lock to single axis |
| announcements | Announcements | Default messages | Screen reader announcements |
| screenReaderInstructions | string | Default text | Instructions for focused items |
Primitives
createDraggable(options) / createSortable(options) / createTreeSortable(options)
All return:
ref— Assign to elementactivatorRef— Assign to drag handle (optional)isDragging()— Is this item being draggedtransform()— Current{ x, y }translationariaAttributes()— ARIA attributes object
createSortable also returns:
isOver()— Is a dragged item over this targetisSettling()— Is FLIP animation playing
createTreeSortable also returns:
projectedDepth()— Projected nesting depth during dragisInActiveSubtree()— Is this item a descendant of the dragged item
createDroppable(options)
Returns:
ref— Assign to elementisOver()— Is a dragged item over this target
Collision detection
| Algorithm | Best for |
|-----------|----------|
| closestCenter | Flat lists, grids |
| rectIntersection | Large drop zones |
| closestToPointer | Nested droppables (Kanban) |
Sort strategies
| Strategy | Use case |
|----------|----------|
| verticalListStrategy | Vertical lists (default) |
| horizontalListStrategy | Horizontal lists |
| createGridStrategy(options?) | CSS grid layouts |
Utilities
// Flat list reordering
moveArrayItem(array, fromIndex, toIndex)
transferArrayItem(source, dest, fromIndex, toIndex)
indexOfId(array, id)
// Tree operations
getSubtreeIds(items, activeId)
computeProjection(items, activeId, overIndex, deltaX, indentSize)
moveTreeItems(items, activeId, projection)Running the demo
pnpm install
pnpm demoLicense
MIT
