@nizalix/arrange-react
v0.1.0
Published
React drag and drop for Arrange: DragDropProvider, useDraggable, useDroppable, useSortable and DragOverlay. Headless, accessible, works with Next.js, Tailwind, Bootstrap or plain CSS.
Maintainers
Readme
@nizalix/arrange-react
Headless drag and drop primitives for React and Next.js. Sortable lists and grids, drag overlays, drop targets, mouse + touch + keyboard, screen-reader announcements.
New here? You probably want
@nizalix/arrange-presets(a ready-made sortable list, grid or table),@nizalix/arrange-kanban(boards) or@nizalix/arrange-tree(nested trees). All of them are built on this package. Use@nizalix/arrange-reactdirectly when you want full control of your markup.
Why use it
- Headless. No CSS, no markup. Works with Tailwind, Bootstrap, CSS Modules, styled-components or plain CSS.
- Accessible by default. Keyboard dragging (Space to pick up, arrows to move, Space to drop, Escape to cancel) and live-region announcements.
- Pointer, touch and keyboard in one API.
- React 18 and 19, Strict Mode safe, SSR safe, TypeScript types included.
- You own the data. Arrange never mutates your arrays. It tells you what moved and you update state.
Install
npm install @nizalix/arrange-reactPeer dependencies: react and react-dom (18 or 19). @nizalix/arrange-core is installed automatically.
Quick start: a sortable list
"use client"; // Next.js App Router: drag and drop runs in the browser
import { useState } from "react";
import { applySortableMove } from "@nizalix/arrange-core";
import { DragDropProvider, SortableContext, useSortable } from "@nizalix/arrange-react";
type Task = { id: string; title: string };
function TaskRow({ task }: { task: Task }) {
const sortable = useSortable({ id: task.id });
return (
<li
ref={sortable.setNodeRef}
style={sortable.style}
{...sortable.attributes}
className={sortable.isDragging ? "row row--dragging" : "row"}
>
{task.title}
</li>
);
}
export function TaskList() {
const [tasks, setTasks] = useState<Task[]>([
{ id: "1", title: "Write the docs" },
{ id: "2", title: "Ship version 1" },
{ id: "3", title: "Celebrate" },
]);
return (
<DragDropProvider
onDragEnd={(event) => {
if (!event.move) return; // dropped outside, or nothing changed
setTasks((current) => {
const next = applySortableMove({ tasks: current }, event.move!, (task) => task.id);
return [...(next.tasks ?? current)];
});
}}
>
<SortableContext id="tasks" items={tasks.map((task) => task.id)}>
<ul>
{tasks.map((task) => (
<TaskRow key={task.id} task={task} />
))}
</ul>
</SortableContext>
</DragDropProvider>
);
}That's a complete sortable list. Try it with the mouse, with touch, and with the keyboard: Tab to a row, press Space, use Arrow keys, press Space to drop (or Escape to cancel).
Rules of thumb:
- Give every item a stable
id(a database id), never the array index. - The
idof<SortableContext>must match the key you use inapplySortableMove. useSortablemust be called inside<SortableContext>, which must be inside<DragDropProvider>.
Add a drag handle
By default the whole row is the drag handle. To drag only by a grip button, attach
setActivatorNodeRef to it:
function TaskRow({ task }: { task: Task }) {
const s = useSortable({ id: task.id });
return (
<li ref={s.setNodeRef} style={s.style}>
<button ref={s.setActivatorNodeRef} {...s.attributes} aria-label={`Drag ${task.title}`}>
⠿
</button>
{task.title}
</li>
);
}(When using a handle, spread s.attributes on the handle, not on the row.)
Show a floating preview (DragOverlay)
import { DragDropProvider, DragOverlay } from "@nizalix/arrange-react";
<DragDropProvider onDragEnd={handleDragEnd}>
{/* ...your SortableContext and rows... */}
<DragOverlay>{(active) => <div className="card">{String(active.data?.label ?? active.id)}</div>}</DragOverlay>
</DragDropProvider>;Pass data={{ label: "My task" }} to useSortable({ id, data }) so the overlay and the screen-reader
announcements can name the item. The overlay is rendered in a portal, follows the pointer without
re-rendering React, and animates into place on drop.
Grids and horizontal lists
<SortableContext id="photos" items={ids} axis="grid">…</SortableContext> // wrapping grid
<SortableContext id="tabs" items={ids} axis="x">…</SortableContext> // horizontal rowMultiple lists (move between containers)
Render several <SortableContext> blocks inside one <DragDropProvider>, each with its own id and
items. Call useSortableContainer() on the element wrapping each list so empty lists accept drops:
function Column({ id, items, children }: { id: string; items: string[]; children: React.ReactNode }) {
return (
<SortableContext id={id} items={items}>
<ColumnBody>{children}</ColumnBody>
</SortableContext>
);
}
function ColumnBody({ children }: { children: React.ReactNode }) {
const container = useSortableContainer();
return <div ref={container.setNodeRef}>{children}</div>;
}In onDragEnd, event.move contains the source and destination container ids and indexes, and
applySortableMove handles moves across containers. For Kanban boards use
@nizalix/arrange-kanban, which does all this for you.
Rejecting drops
<DragDropProvider
canDrop={({ active, over }) => (over?.id === "locked" ? { ok: false, reason: "That list is locked." } : true)}
onDropRejected={(event) => alert(event.reason)}
/>Items can also be disabled: useSortable({ id, disabled: true }).
Styling hooks
| From useSortable | Meaning |
| --- | --- |
| isDragging | This item is being dragged (or settling after a drop) |
| isOver / isInvalidOver | The dragged item is over this one / over it but the drop would be rejected |
| dropPosition | "before" or "after" while this item is the drop target |
| style | Transform and transition that make room for the dragged item |
Do not rely on color alone to show an invalid drop; also show text or an icon.
API reference
<DragDropProvider>
| Prop | Description |
| --- | --- |
| onDragStart, onDragMove, onDragOver, onDragEnd, onDragCancel | Event handlers. onDragEnd receives event.move for sortable drops |
| sensors | Input sensors. Default: pointer (mouse, pen, touch) and keyboard |
| collisionDetection | How the drop target is chosen. Default works for lists, grids and nested containers |
| modifiers | Constrain movement, e.g. restrictToVerticalAxis from @nizalix/arrange-core |
| autoScroll | false to disable edge auto-scroll, or options |
| animation | { duration, easing } for drop animations. Respects reduced-motion |
| accessibility | Customize announcements and instructions text |
| canDrop, validateDrop, canActivate | Veto drags and drops with an optional reason |
| plugins | Engine plugins (memoize the array) |
Hooks and components
| Export | Purpose |
| --- | --- |
| SortableContext | Declares a container: id, items (ordered ids), axis ("y" default, "x", "grid") |
| useSortable({ id, data?, disabled? }) | One sortable item |
| useSortableContainer() | Makes the container element a drop target (empty lists) |
| useDraggable({ id, data?, disabled? }) | Low-level draggable |
| useDroppable({ id, data?, disabled?, accepts? }) | Low-level drop target |
| DragOverlay | Floating preview rendered in a portal |
| useDragState, useDragMotion | Read the current drag state / pointer motion |
| useControllableState | Controlled / uncontrolled state helper |
Every type is exported with the package, so your editor shows documentation as you type.
Troubleshooting
- "must be used inside
<SortableContext>":useSortablehas to render belowSortableContext, which has to render belowDragDropProvider. - Nothing happens in Next.js: the component needs
"use client"at the top of the file. - Items snap back after dropping: you must update your state in
onDragEnd. Arrange never reorders your data for you. - Wrong item moves: ids must be stable and unique; do not use the array index.
- Scrolling on touch screens fights the drag: drag from a handle, or set
touch-action: noneon the handle.
Related packages
@nizalix/arrange-presets ·
@nizalix/arrange-kanban ·
@nizalix/arrange-tree ·
@nizalix/arrange-core ·
@nizalix/arrange-nestjs-adapter
Author
Created and maintained by salmannizam. Issues and ideas are welcome on GitHub. MIT licensed.
