> ## Documentation Index
> Fetch the complete documentation index at: https://dndkit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Multiple sortable lists

> Learn how to build kanban boards, trello-like interfaces, and multi-column layouts with sortable drag and drop across multiple lists.

export const CodeSandbox = ({files, height, hero, previewHeight, showTabs, template}) => {
  const [Editor, setEditor] = useState(null);
  useEffect(() => {
    import('@components/CodeSandbox').then(mod => {
      setEditor(() => mod.CodeSandbox);
    });
  }, []);
  if (!Editor) return null;
  return <div className={`not-prose${hero ? ' hero' : ''}`} style={previewHeight ? {
    '--preview-height': `${previewHeight}px`
  } : undefined}>
      <Editor files={files} height={height} showTabs={showTabs} template={template} />
    </div>;
};

export const Story = ({id, framework = "react", width = "100%", height = "250", hero = false}) => {
  const BRANCH = 'experimental';
  const STORYBOOKS = {
    react: {
      localPort: 6006,
      productionHost: '5fc05e08a4a65d0021ae0bf2'
    },
    vue: {
      localPort: 6008,
      productionHost: '6989440ed560d70abcd6bcc7'
    },
    vanilla: {
      localPort: 6007,
      productionHost: '69892d294eb9040f0d29aa81'
    },
    solid: {
      localPort: 6009,
      productionHost: '698944444eb9040f0d2a0217'
    },
    svelte: {
      localPort: 6010,
      productionHost: '69910d2a631cb57638616dcd'
    }
  };
  const config = STORYBOOKS[framework] ?? STORYBOOKS.react;
  const isDev = import.meta.env.DEV;
  const host = isDev ? `//localhost:${config.localPort}` : `https://${BRANCH}--${config.productionHost}.chromatic.com`;
  return <Frame>
      <iframe src={`${host}/iframe.html?args=&id=${id}&viewMode=story&hero=${hero}`} width={width} height={height} />
    </Frame>;
};

<Story id="react-sortable-multiple-lists--hero" height="400" hero />

## Overview

In this guide, you'll learn how to reorder sortable elements across multiple lists. This pattern is commonly used to build **kanban boards**, **trello-like task managers**, and **multi-column layouts** where items can be reordered within and across columns.

Before getting started, make sure you familiarize yourself with the [useSortable](/react/hooks/use-sortable) 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.

<CodeSandbox
  height="560"
  previewHeight="260"
  files={{
"App.js": App,
"Column.js": Column,
"Item.js": Item,
"styles.css": Styles
}}
  showTabs
/>

## Adding drag and drop functionality

Now, let's add drag and drop functionality to the items. We'll be using the [useSortable](/react/hooks/use-sortable) hook to make the items sortable. Let's modify the `Item` component to make it sortable:

<CodeSandbox
  height="435"
  previewHeight="260"
  files={{
...files,
"Item.js": {code: SortableItem, active: true},
}}
  showTabs
/>

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](/react/hooks/use-droppable) hook to create a drop target for each column. Let's modify the `Column` component to make it droppable:

<CodeSandbox
  height="455"
  previewHeight="260"
  files={{
...files,
"Column.js": {code: DroppableColumn, active: true},
}}
  showTabs
/>

<Note>We're setting the `collisionPriority` to `CollisionPriority.Low` to prioritize collisions of items over collisions of columns. Learn more about [detecting collisions](/concepts/droppable#detecting-collisions).</Note>

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](/react/components/drag-drop-provider) 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:

<CodeSandbox
  height="455"
  previewHeight="260"
  files={{
...files,
"App.js": {code: AppDroppableColumns, active: true},
"Column.js": {code: DroppableColumn},
}}
  showTabs
/>

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:

<CodeSandbox
  height="455"
  previewHeight="260"
  files={{
...files,
"App.js": {code: AppSortableColumns},
"Column.js": {code: SortableColumn, active: true},
}}
  showTabs
/>

<Info>You'll also need to pass the column `index` prop to the `Column` component in the `App` component.</Info>

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:

```jsx App.js theme={null}
export function App() {
  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, event));
      }}
    >
      <div className="Root">
        {columnOrder.map((column, columnIndex) => (
          <Column key={column} id={column} index={columnIndex}>
            {items[column].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](/extend/sensors/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:

```jsx App.js theme={null}
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">
        {columnOrder.map((column, columnIndex) => (
          <Column key={column} id={column} index={columnIndex}>
            {items[column].map((id, index) => (
              <Item key={id} id={id} index={index} column={column} />
            ))}
          </Column>
        ))}
      </div>
    </DragDropProvider>
  );
}
```

<Note>Optimistic updates performed by `@dnd-kit` are automatically reverted when a drag operation is canceled.</Note>

export const App = `import React, {useState} from 'react';
import {Column} from './Column.js';
import {Item} from './Item.js';
import './styles.css';

export default function App() {
  const [items] = useState({
    A: ['A0', 'A1', 'A2'],
    B: ['B0', 'B1'],
    C: [],
  });

  return (
    <div className="Root">
      {Object.entries(items).map(([column, items]) => (
        <Column key={column} id={column}>
          {items.map((id, index) => (
            <Item key={id} id={id} index={index} column={column} />
          ))}
        </Column>
      ))}
    </div>
  );
}`;

export const AppDroppableColumns = `import React, {useState} from 'react';
import {DragDropProvider} from '@dnd-kit/react';
import {move} from '@dnd-kit/helpers';
import {Column} from './Column.js';
import {Item} from './Item.js';
import './styles.css';

export default function App() {
  const [items, setItems] = useState({
    A: ['A0', 'A1', 'A2'],
    B: ['B0', 'B1'],
    C: [],
  });

  return (
    <DragDropProvider
      onDragOver={(event) => {
        setItems((items) => move(items, event));
      }}
    >
      <div className="Root">
        {Object.entries(items).map(([column, items]) => (
          <Column key={column} id={column}>
            {items.map((id, index) => (
              <Item key={id} id={id} index={index} column={column} />
            ))}
          </Column>
        ))}
      </div>
    </DragDropProvider>
  );
}`;

export const AppSortableColumns = `import React, {useState} from 'react';
import {DragDropProvider} from '@dnd-kit/react';
import {move} from '@dnd-kit/helpers';
import {Column} from './Column.js';
import {Item} from './Item.js';
import './styles.css';

export default function App() {
  const [items, setItems] = useState({
    A: ['A0', 'A1', 'A2'],
    B: ['B0', 'B1'],
    C: [],
  });

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

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

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

export const Column = `import React from 'react';

export function Column({children, id}) {
  return (
    <div className="Column">
      {children}
    </div>
  );
}`;

export const DroppableColumn = `import React from 'react';
import {useDroppable} from '@dnd-kit/react';
import {CollisionPriority} from '@dnd-kit/abstract';

export function Column({children, id}) {
  const {isDropTarget, ref} = useDroppable({
    id,
    type: 'column',
    accept: 'item',
    collisionPriority: CollisionPriority.Low,
  });
  const style = isDropTarget ? {background: '#00000030'} : undefined;

  return (
    <div className="Column" ref={ref} style={style}>
      {children}
    </div>
  );
}`;

export const SortableColumn = `import React from 'react';
import {CollisionPriority} from '@dnd-kit/abstract';
import {useSortable} from '@dnd-kit/react/sortable';

export function Column({children, id, index}) {
  const {ref} = useSortable({
    id,
    index,
    type: 'column',
    collisionPriority: CollisionPriority.Low,
    accept: ['item', 'column'],
  });

  return (
    <div className="Column" ref={ref}>
      {children}
    </div>
  );
}`;

export const Item = `import React from 'react';

export function Item({id, index}) {
  return (
    <button className="Item">
      {id}
    </button>
  );
}`;

export const SortableItem = `import React from 'react';
import {useSortable} from '@dnd-kit/react/sortable';

export function Item({id, index, column}) {
  const {ref, isDragging} = useSortable({
    id,
    index,
    type: 'item',
    accept: 'item',
    group: column
  });

  return (
    <button className="Item" ref={ref} data-dragging={isDragging}>
      {id}
    </button>
  );
}`;

export const Styles = `
.Root {
  display: flex;
  flex-direction: row;
  gap: 20px;
  flex-wrap: wrap;
}

.Column {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px;
  min-width: 175px;
  min-height: 200px;
  background-color: rgba(0, 0, 0, 0.1);
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 10px;
}

.Item {
  appearance: none;
  background: #FFF;
  color: #666;
  padding: 12px 20px;
  border: none;
  border-radius: 5px;
  cursor: grab;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  transform: scale(1);
  box-shadow: inset 0px 0px 1px rgba(0,0,0,0.4), 0 0 0 calc(1px / var(--scale-x, 1)) rgba(63, 63, 68, 0.05), 0px 1px calc(2px / var(--scale-x, 1)) 0 rgba(34, 33, 81, 0.05);
}

.Item[data-dragging="true"] {
  transform: scale(1.02);
  box-shadow: inset 0px 0px 1px rgba(0,0,0,0.5), -1px 0 15px 0 rgba(34, 33, 81, 0.01), 0px 15px 15px 0 rgba(34, 33, 81, 0.25)
}`;

export const files = {
  "Item.js": {
    code: SortableItem,
    hidden: true
  },
  "App.js": {
    code: App,
    hidden: true
  },
  "Column.js": {
    code: Column,
    hidden: true
  },
  "styles.css": {
    code: Styles,
    hidden: true
  }
};
