Overview

In this guide, you’ll learn how to reorder sortable elements across multiple lists. This is useful when you have multiple lists and you want to move elements between them.

Before getting started, make sure you familiarize yourself with the useSortable hook.

We’ll be setting up three columns, and each column will have its own list of items. You’ll be able to drag and drop items between the columns.

Setup

First, let’s set up the initial setup for the columns and items. We’ll be creating three files, App.js, Column.js, and Item.js, and applying some basic styles in the Styles.css file.

Adding drag and drop functionality

Now, let’s add drag and drop functionality to the items. We’ll be using the useSortable hook to make the items sortable. Let’s modify the Item component to make it sortable:

As you can see, we’ve added the useSortable hook to the Item component. We’ve also passed the id, index, type, accept, and group props to the hook.

This creates an uncontrolled list of sortable items that can be sorted within each column, and across columns thanks to the group property. However, in order to be able to move items to an empty column, we need to add some additional logic.

Moving items between columns

To move items to empty columns, we need to add make each column droppable.

We’ll be using the useDroppable hook to create a drop target for each column. Let’s modify the Column component to make it droppable:

We’re setting the collisionPriority to CollisionPriority.Low to prioritize collisions of items over collisions of columns. Learn more about detecting collisions.

This will allow us to drop items into each column. However, we still need to handle the logic for moving items between columns.

We’ll be using the DragDropProvider component to listen and respond to the drag and drop events. Let’s modify the App component to add the DragDropProvider. We’ll be using the move helper function from @dnd-kit/helpers to help us mutate the array of items between columns:

As you can see, we’ve added the DragDropProvider component to the App component. We’ve also added an onDragOver event handler to listen for drag and drop events.

When an item is dragged over a column, the onDragOver event handler will be called. We’ll use the move helper function to move the item between columns.

The result is a sortable list of items that can be moved between columns.

Making the columns sortable

If you want to make the columns themselves sortable, you can use the useSortable hook in the Column component. Here’s how you can modify the Column component to make it sortable:

You’ll also need to pass the column index prop to the Column component in the App component.

If we want to control the state of the columns in React, we can update the App component to handle the column order in the onDragEnd callback:

App.js
export function App({style = styles}) {
  const [items, setItems] = useState({
    A: ['A0', 'A1', 'A2'],
    B: ['B0', 'B1'],
    C: [],
  });
  const [columnOrder, setColumnOrder] = useState(() => Object.keys(items));

  return (
    <DragDropProvider
      onDragOver={(event) => {
        const {source, target} = event.operation;

        if (source?.type === 'column') return;

        setItems((items) => move(items, event));
      }}
      onDragEnd={(event) => {
        const {source, target} = event.operation;

        if (event.canceled || source.type !== 'column') return;

        setColumnOrder((columns) => move(columns, sevent));
      }}
    >
      <div className="Root">
        {Object.entries(items).map(([column, items], columnIndex) => (
          <Column key={column} id={column} index={columnIndex}>
            {items.map((id, index) => (
              <Item key={id} id={id} index={index} column={column} />
            ))}
          </Column>
        ))}
      </div>
    </DragDropProvider>
  );
}

We’re using the onDragEnd event handler instead of the onDragOver event handler to handle the column order. This allows us to only update the order of the columns in React when the drag operation is completed, while letting @dnd-kit optimistically update the order of the columns during the drag operation, without causing unnecessary re-renders. If we wanted more control over the drag and drop operation, we could also handle the event in the onDragOver callback.

Handling canceled drag operations

It’s possible for a drag operation to be canceled. For example, users can cancel a drag operation initiated by the Pointer sensor by pressing the Escape key.

When you update the order of items in the onDragOver callback, you should make sure to check if the user decided to abort the drag operation in the onDragEnd callback. If the drag operation was canceled, you should revert the order of items to the state before the drag operation started.

For example, here is how we would update our app to handle this case for the order of items:

App.js
import React, {useRef, useState} from 'react';
import {DragDropProvider} from '@dnd-kit/react';
import {move} from '@dnd-kit/helpers';
import "./Styles.css";

import {Column} from './Column';
import {Item} from './Item';

export function App({style = styles}) {
  const [items, setItems] = useState({
    A: ['A0', 'A1', 'A2'],
    B: ['B0', 'B1'],
    C: [],
  });
  const previousItems = useRef(items);
  const [columnOrder, setColumnOrder] = useState(() => Object.keys(items));

  return (
    <DragDropProvider
      onDragStart={() => {
        previousItems.current = items;
      }}
      onDragOver={(event) => {
        const {source, target} = event.operation;

        if (source?.type === 'column') return;

        setItems((items) => move(items, event));
      }}
      onDragEnd={(event) => {
        const {source, target} = event.operation;

        if (event.canceled) {
          if (source.type === 'item') {
            setItems(previousItems.current);
          }

          return;
        }

        if (source.type === 'column') {
          setColumnOrder((columns) => move(columns, event));
        }
      }}
    >
      <div className="Root">
        {Object.entries(items).map(([column, items], columnIndex) => (
          <Column key={column} id={column} index={columnIndex}>
            {items.map((id, index) => (
              <Item key={id} id={id} index={index} column={column} />
            ))}
          </Column>
        ))}
      </div>
    </DragDropProvider>
  );
}
Optimistic updates performed by @dnd-kit are automatically reverted when a drag operation is canceled.