Skip to main content

Overview

The Sortable class combines Draggable and Droppable functionality to create sortable lists, grids, and other reorderable collections. It handles index tracking, smooth transitions, and optimistic updates automatically.

Installation

import { Sortable } from '@dnd-kit/dom/sortable';

Constructor

const sortable = new Sortable(input, manager);

Parameters

input
SortableInput
required
Configuration for the sortable item
manager
DragDropManager
required
The DragDropManager instance

Properties

index

sortable.index = 2;
console.log(sortable.index); // 2
Current index within the sortable group. Updating this triggers a transition animation.

initialIndex

console.log(sortable.initialIndex); // Index when drag started
The index when the current drag operation started. Read-only.

group

sortable.group = 'list-1';
console.log(sortable.group); // 'list-1'
Group identifier. Items in the same group can be sorted together.

initialGroup

console.log(sortable.initialGroup); // Group when drag started
The group when the current drag operation started. Read-only.

element

sortable.element = document.getElementById('item-1');
The sortable’s DOM element.

target

sortable.target = document.getElementById('drop-zone-1');
The droppable target element. Defaults to element.

isDragSource

if (sortable.isDragSource) {
  console.log('This item is being dragged');
}
Whether this sortable is the source of the current drag. Read-only.

isDragging

if (sortable.isDragging) {
  console.log('This item is being dragged');
}
Whether this sortable is currently being dragged. Read-only.

isDropTarget

if (sortable.isDropTarget) {
  console.log('Something is being dragged over this item');
}
Whether something is being dragged over this sortable. Read-only.

disabled

sortable.disabled = true;
Disable both dragging and dropping for this sortable.

Methods

register()

const unregister = sortable.register();

// Later...
unregister();
Register the sortable with the manager. Returns a function to unregister.

unregister()

sortable.unregister();
Unregister the sortable from the manager.

destroy()

sortable.destroy();
Destroy the sortable and clean up resources.

refreshShape()

sortable.refreshShape();
Refresh the sortable’s bounding rectangle. Useful after layout changes.

Default Transition

const defaultSortableTransition: SortableTransition = {
  duration: 250,                           // Milliseconds
  easing: 'cubic-bezier(0.25, 1, 0.5, 1)', // Smooth easing
  idle: false                               // Don't animate when not dragging
};

Basic Example

import { DragDropManager } from '@dnd-kit/dom';
import { Sortable } from '@dnd-kit/dom/sortable';

const manager = new DragDropManager();

const items = ['Item 1', 'Item 2', 'Item 3'];
const sortables: Sortable[] = [];

items.forEach((item, index) => {
  const element = document.getElementById(`item-${index}`);
  
  const sortable = new Sortable(
    {
      id: `item-${index}`,
      index,
      element
    },
    manager
  );
  
  sortable.register();
  sortables.push(sortable);
});

// Update indices on dragend
manager.monitor.addEventListener('dragend', (event) => {
  if (!event.target) return;
  
  // Reorder logic here
  const oldIndex = event.source.index;
  const newIndex = event.target.index;
  
  // Update your data model
  const [removed] = items.splice(oldIndex, 1);
  items.splice(newIndex, 0, removed);
  
  // Update sortable indices
  sortables.forEach((sortable, i) => {
    sortable.index = i;
  });
});

Multi-List Example

import { DragDropManager } from '@dnd-kit/dom';
import { Sortable } from '@dnd-kit/dom/sortable';

const manager = new DragDropManager();

const lists = {
  'list-1': ['A', 'B', 'C'],
  'list-2': ['D', 'E', 'F']
};

const sortables = new Map<string, Sortable>();

Object.entries(lists).forEach(([listId, items]) => {
  items.forEach((item, index) => {
    const element = document.getElementById(`${listId}-item-${index}`);
    
    const sortable = new Sortable(
      {
        id: `${listId}-${index}`,
        index,
        group: listId,  // Items in same group can sort together
        element,
        accept: 'sortable-item', // Accept any sortable item
        type: 'sortable-item'
      },
      manager
    );
    
    sortable.register();
    sortables.set(sortable.id, sortable);
  });
});

manager.monitor.addEventListener('dragend', (event) => {
  if (!event.target) return;
  
  const source = event.source as Sortable;
  const target = event.target as Sortable;
  
  const sourceList = source.initialGroup;
  const targetList = target.group;
  
  // Same list reorder
  if (sourceList === targetList) {
    const items = lists[sourceList];
    const [removed] = items.splice(source.initialIndex, 1);
    items.splice(target.index, 0, removed);
  }
  // Cross-list move
  else {
    const sourceItems = lists[sourceList];
    const targetItems = lists[targetList];
    const [removed] = sourceItems.splice(source.initialIndex, 1);
    targetItems.splice(target.index, 0, removed);
  }
  
  // Update all indices
  Object.entries(lists).forEach(([listId, items]) => {
    items.forEach((item, index) => {
      const sortable = sortables.get(`${listId}-${index}`);
      if (sortable) {
        sortable.index = index;
        sortable.group = listId;
      }
    });
  });
});

Grid Example

import { DragDropManager } from '@dnd-kit/dom';
import { Sortable } from '@dnd-kit/dom/sortable';

const manager = new DragDropManager();

const GRID_COLS = 4;
const items = Array.from({ length: 12 }, (_, i) => `Item ${i + 1}`);

const sortables = items.map((item, index) => {
  const element = document.getElementById(`grid-item-${index}`);
  
  const sortable = new Sortable(
    {
      id: `grid-${index}`,
      index,
      element,
      transition: {
        duration: 200,
        easing: 'ease-out'
      }
    },
    manager
  );
  
  sortable.register();
  return sortable;
});

manager.monitor.addEventListener('dragend', (event) => {
  if (!event.target) return;
  
  const oldIndex = event.source.index;
  const newIndex = event.target.index;
  
  // Reorder items
  const [removed] = items.splice(oldIndex, 1);
  items.splice(newIndex, 0, removed);
  
  // Update all indices
  sortables.forEach((sortable, i) => {
    sortable.index = i;
  });
});

Custom Transitions

const sortable = new Sortable(
  {
    id: 'item-1',
    index: 0,
    element: document.getElementById('item-1'),
    transition: {
      duration: 300,
      easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
      idle: true  // Animate even when not dragging
    }
  },
  manager
);

// Or disable transitions
const sortableNoTransition = new Sortable(
  {
    id: 'item-2',
    index: 1,
    element: document.getElementById('item-2'),
    transition: null  // No animation
  },
  manager
);

Built-in Plugins

Sortable includes two plugins by default:

SortableKeyboardPlugin

Enhances keyboard navigation for sortable lists:
  • Up/Down arrow keys move between items
  • Automatically reorders on arrow key press
  • Works with screen readers

OptimisticSortingPlugin

Provides optimistic updates during dragging:
  • Items animate to their new positions before drop
  • Smooth reordering as you drag over items
  • Automatically reverts if drag is canceled

Type Definitions

interface SortableInput<T extends Data>
  extends DraggableInput<T>, DroppableInput<T> {
  index: number;
  target?: Element;
  group?: UniqueIdentifier;
  transition?: SortableTransition | null;
  plugins?: Plugins;
}

interface SortableTransition {
  duration?: number;
  easing?: string;
  idle?: boolean;
}

class Sortable<T extends Data = Data> {
  constructor(input: SortableInput<T>, manager: DragDropManager);
  
  index: number;
  readonly initialIndex: number;
  group: UniqueIdentifier | undefined;
  readonly initialGroup: UniqueIdentifier | undefined;
  
  element: Element | undefined;
  target: Element | undefined;
  
  readonly isDragSource: boolean;
  readonly isDragging: boolean;
  readonly isDropTarget: boolean;
  
  disabled: boolean;
  
  register(): () => void;
  unregister(): void;
  destroy(): void;
  refreshShape(): Rectangle | undefined;
}

Additional Exports

SortableDraggable

A specialized Draggable class optimized for sortable items:
import { SortableDraggable } from '@dnd-kit/dom/sortable';

class SortableDraggable<T extends Data = Data> extends Draggable<T> {
  // Specialized for sortable use cases
}

SortableDroppable

A specialized Droppable class optimized for sortable drop targets:
import { SortableDroppable } from '@dnd-kit/dom/sortable';

class SortableDroppable<T extends Data = Data> extends Droppable<T> {
  // Specialized for sortable use cases
}

defaultSortableTransition

The default transition configuration for sortable items:
import { defaultSortableTransition } from '@dnd-kit/dom/sortable';

const defaultTransition: SortableTransition = {
  duration: 250,
  easing: 'ease',
  idle: false
};

Utility Functions

isSortable

Type guard to check if an entity is a sortable:
import { isSortable } from '@dnd-kit/dom/sortable';

if (isSortable(entity)) {
  console.log('Index:', entity.index);
  console.log('Group:', entity.group);
}

isSortableOperation

Type guard to check if a drag operation involves sortables:
import { isSortableOperation } from '@dnd-kit/dom/sortable';

manager.monitor.addEventListener('dragover', (event) => {
  if (isSortableOperation(event.operation)) {
    console.log('Source index:', event.operation.source.index);
    console.log('Target index:', event.operation.target?.index);
  }
});

Build docs developers (and LLMs) love