apps/docs/concepts/sortable.mdx
import {Story} from '/snippets/story.mdx'; import {CodeSandbox} from '/snippets/sandbox.mdx'; import {sortableStyles} from '/snippets/code.mdx';
<Story id="react-sortable--example" height="320" hero />The Sortable class allows you to reorder elements in a list or across multiple lists. A sortable element is both Droppable and Draggable, which means you can drag it and drop it to reorder.
First, create a DragDropManager instance and use it to create sortable items:
export const code = ` import {DragDropManager} from '@dnd-kit/dom'; import {Sortable} from '@dnd-kit/dom/sortable';
export function App() { const manager = new DragDropManager();
const wrapper = document.createElement('ul'); const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];
wrapper.classList.add('list');
items.forEach((item, index) => { const element = document.createElement('li');
element.classList.add('item');
element.innerText = item;
const sortable = new Sortable({
id: item,
index, // Required - the position in the list
element,
}, manager);
wrapper.appendChild(element);
});
document.body.appendChild(wrapper); } `.trim();
<CodeSandbox files={{
'index.js': {code: import './styles.css';\nimport {App} from './sortable.js';\n\nApp();, hidden: true},
'sortable.js': {code, active: true},
'styles.css': {code: sortableStyles, hidden: true},
}} height={580} previewHeight={180} template="vanilla" />
You can create multiple sortable lists by assigning sortable items to different groups:
const list1 = ['Item 1', 'Item 2'];
const list2 = ['Item 3', 'Item 4'];
// First list
list1.forEach((item, index) => {
new Sortable({
id: item,
index,
group: 'list1', // Assign to first group
element: createItemElement(item),
}, manager);
});
// Second list
list2.forEach((item, index) => {
new Sortable({
id: item,
index,
group: 'list2', // Assign to second group
element: createItemElement(item),
}, manager);
});
By default, the entire sortable element can be used to initiate dragging. You can restrict dragging to a specific handle element:
const element = document.createElement('li');
const handle = document.createElement('div');
handle.classList.add('handle');
element.appendChild(handle);
new Sortable({
id: 'item-1',
index: 0,
element,
handle, // Only allow dragging from the handle
}, manager);
Sortable items automatically animate when their position changes. You can customize the animation through the transition option:
new Sortable({
id: 'item-1',
index: 0,
transition: {
duration: 250, // Animation duration in ms
easing: 'cubic-bezier(0.25, 1, 0.5, 1)', // Animation easing
idle: false, // Whether to animate when no drag is in progress
}
}, manager);
By default, every Sortable instance registers the OptimisticSortingPlugin. This plugin optimistically reorders DOM elements during a drag operation so that the UI feels responsive — without requiring your framework to re-render on every dragover event.
When you drag a sortable item over another sortable item, the plugin:
index (and group, for multi-list scenarios) on each affected Sortable instance.manager.actions.setDropTarget(source.id).Step 3 has an important consequence: during a drag, source and target on the operation will refer to the same element. This also means that isDragSource and isDropTarget will both be true on the dragged item.
Since source and target are the same, you cannot compare their IDs to determine what moved. Instead, use the sortable-specific properties available on the source:
| Property | Description |
|---|---|
index | The current position of the item (updated by the plugin as it moves) |
initialIndex | The position the item was in when the drag started |
group | The current group the item belongs to |
initialGroup | The group the item was in when the drag started |
If you call event.preventDefault() in a dragover handler, the OptimisticSortingPlugin will skip the optimistic update for that particular event. This is useful when you want to handle certain moves yourself (for example, to prevent items from being dragged into a specific group) while still letting the plugin handle the rest:
manager.monitor.addEventListener('dragover', (event) => {
const {source, target} = event.operation;
if (shouldPreventMove(source, target)) {
event.preventDefault(); // Optimistic sorting will not run for this event
}
});
If you prefer to manage sorting entirely in your application state (for example, by handling every dragover event), you can disable optimistic sorting by omitting the OptimisticSortingPlugin from the plugins array:
import {SortableKeyboardPlugin} from '@dnd-kit/dom/sortable';
new Sortable({
id: 'item-1',
index: 0,
element,
plugins: [SortableKeyboardPlugin], // No OptimisticSortingPlugin
}, manager);
Without optimistic sorting, source and target will be different elements during drag, and you can use their IDs directly. However, you will need to handle reordering in your dragover listener for smooth visual feedback.
The move helper from @dnd-kit/helpers is a convenience function that takes your items and a drag event and returns a new array with the item moved to its new position. It supports flat arrays and grouped records, handles canceled drags, and works with optimistic sorting out of the box.
If you need more control over state updates, you can manage state manually using the sortable properties and the isSortable type guard.
With optimistic sorting enabled (the default), you only need to handle the dragend event. The isSortable type guard narrows the source to expose initialIndex and index:
import {isSortable} from '@dnd-kit/dom/sortable';
manager.monitor.addEventListener('dragend', (event) => {
if (event.canceled) return;
const {source} = event.operation;
if (isSortable(source)) {
const {initialIndex, index} = source;
if (initialIndex !== index) {
// Reorder your data: move the item from initialIndex to index
const newItems = [...items];
const [removed] = newItems.splice(initialIndex, 1);
newItems.splice(index, 0, removed);
items = newItems;
}
}
});
For multiple lists, use initialGroup and group to detect whether the item stayed in the same list or moved to a different one:
manager.monitor.addEventListener('dragend', (event) => {
if (event.canceled) return;
const {source} = event.operation;
if (isSortable(source)) {
const {initialIndex, index, initialGroup, group} = source;
if (initialGroup === group) {
// Same group: reorder within the list
const groupItems = [...items[group]];
const [removed] = groupItems.splice(initialIndex, 1);
groupItems.splice(index, 0, removed);
items = {...items, [group]: groupItems};
} else {
// Cross-group transfer
const sourceItems = [...items[initialGroup]];
const [removed] = sourceItems.splice(initialIndex, 1);
const targetItems = [...items[group]];
targetItems.splice(index, 0, removed);
items = {...items, [initialGroup]: sourceItems, [group]: targetItems};
}
}
});
@dnd-kit/dom/sortable exports two type guards to help you work with sortable drag operations.
isSortableChecks whether a Draggable or Droppable instance is a sortable element. If it returns true, the type is narrowed to expose sortable-specific properties like index, initialIndex, group, and initialGroup.
import {isSortable} from '@dnd-kit/dom/sortable';
const {source} = event.operation;
if (isSortable(source)) {
console.log(source.index); // number
console.log(source.initialIndex); // number
console.log(source.group); // string | number | undefined
console.log(source.initialGroup); // string | number | undefined
}
isSortableOperationChecks whether both source and target of a drag operation are sortable elements. This is useful when you want to narrow the entire operation at once:
import {isSortableOperation} from '@dnd-kit/dom/sortable';
const {operation} = event;
if (isSortableOperation(operation)) {
// Both source and target are narrowed to sortable types
console.log(operation.source.initialIndex);
console.log(operation.target.index);
}
Both type guards are also available from framework-specific packages:
@dnd-kit/react/sortable@dnd-kit/vue/sortable@dnd-kit/svelte/sortable@dnd-kit/solid/sortableThe Sortable class accepts the following arguments:
The Sortable instance provides these key properties:
index: The current position in the listgroup: The assigned group identifierisDragging: Whether this item is currently being draggedisDropTarget: Whether this item is currently a drop targetdisabled: Whether sorting is disabled for this itemelement: The main DOM elementtarget: The drop target element (if different from main element)register(): Register this sortable item with the managerunregister(): Remove this item from the managerdestroy(): Clean up this sortable instanceaccepts(draggable): Check if this item accepts a draggablerefreshShape(): Recalculate the item's dimensions