TL;DR: @hello-pangea/dnd is the maintained continuation of react-beautiful-dnd, with the same API on current React. Best-in-class keyboard and screen-reader support for list reordering. You own the state: onDragEnd gives indices, you produce the new array.
What is Hello Pangea DnD?
Atlassian's react-beautiful-dnd set the standard for list drag-and-drop in React, then stopped being maintained and was archived in 2023, stranding a large number of applications on a package that didn't support React 18.
This fork picked it up. It kept the API, ported the source to TypeScript, added support for React 18 and 19, and has been shipping releases since. For most codebases the upgrade path is changing one string in the import.
When to use it
Use it for lists. Kanban columns, reorderable playlists, a settings screen where the user arranges their own order, a form builder with a field list. Anything where items live in one dimension and move within or between containers.
Use dnd-kit when the interaction isn't list-shaped: free positioning on a canvas, custom collision rules, dragging between fundamentally different surfaces. Neither library is a general answer.
Key Features
- Keyboard dragging out of the box, with space to lift and arrows to move
- Screen-reader announcements at every step of a drag, with overridable text
- Movement driven by transforms only, so dragging never triggers layout
- Multi-list dragging with per-list drop rules
- Virtual list support for long collections
- Types included, so no separate @types package
Installation
npm install @hello-pangea/dnd
Migrating from the original is two steps:
npm uninstall react-beautiful-dnd @types/react-beautiful-dnd
npm install @hello-pangea/dnd
Then replace from 'react-beautiful-dnd' with from '@hello-pangea/dnd'.
A Reorderable List
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
import type { DropResult } from '@hello-pangea/dnd';
import { useState } from 'react';
type Task = { id: string; title: string };
export function TaskList({ initial }: { initial: Task[] }) {
const [tasks, setTasks] = useState(initial);
function onDragEnd(result: DropResult): void {
if (!result.destination) { return; }
if (result.destination.index === result.source.index) { return; }
const next = Array.from(tasks);
const [moved] = next.splice(result.source.index, 1);
next.splice(result.destination.index, 0, moved);
setTasks(next);
}
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="tasks">
{provided => (
<ul ref={provided.innerRef} {...provided.droppableProps}>
{tasks.map((task, index) => (
<Draggable key={task.id} draggableId={task.id} index={index}>
{dragProvided => (
<li
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
>
{task.title}
</li>
)}
</Draggable>
))}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
);
}
Three details do most of the work. provided.placeholder holds the gap open while an item is lifted; without it the list collapses and jumps. draggableId must be a string and must be stable across renders, so an array index will not do. And onDragEnd returning early on a null destination is what makes a drop outside the list a no-op rather than a crash.
Separating the Handle
Split dragHandleProps off the item to make only part of it draggable:
<li ref={dragProvided.innerRef} {...dragProvided.draggableProps}>
<span {...dragProvided.dragHandleProps} aria-label="Reorder">::</span>
{task.title}
</li>
Give the handle an accessible name. It's a focusable control, and without one a screen reader announces nothing useful.
TypeScript Tips
DropResult, DraggableProvided and DroppableProvided are all exported. Typing the onDragEnd parameter as DropResult is usually the only annotation you need, since the render-prop arguments infer.
Common Gotchas
Unstable draggableId is the most common bug: using the array index means ids shift the moment something moves, and items teleport. Use a real id.
Second, React.StrictMode in development double-invokes effects and this library is sensitive to it. If dragging misbehaves only in dev, that's usually why.
Third, a scrollable ancestor with overflow: hidden clips the dragging item. The library lifts items with transforms, so the clip is real. Give the scroll container overflow: auto instead.