Skip to main content

Overview

Once you’ve mastered the basics, you’ll often need to handle multiple drop containers. This guide shows you how to create a drag and drop interface where items can be moved between multiple containers, like a kanban board or card organizer.

What You’ll Build

A multi-container drag and drop interface featuring:
  • Multiple drop zones with unique identities
  • Ability to drag items between any container
  • Visual feedback showing which container will receive the item
  • State management for tracking items across containers

Prerequisites

This guide assumes you understand the basics from the Simple Drag and Drop guide.

Implementation

1
Define Your Container Structure
2
Start by defining your containers and their initial state. Use an object where keys are container IDs and values are arrays of item IDs.
3
import { useState } from 'react';

function App() {
  const [items, setItems] = useState({
    A: ['A1', 'A2', 'A3'],
    B: ['B1', 'B2', 'B3'],
    C: ['C1', 'C2', 'C3'],
    D: []
  });

  // Containers can be derived from the items object
  const containers = Object.keys(items); // ['A', 'B', 'C', 'D']
}
4
Key concepts:
5
  • Each container has a unique ID (A, B, C, D)
  • Items are stored as arrays within each container
  • Empty arrays represent empty containers
  • This structure makes it easy to move items between containers
  • 6
    Create Reusable Draggable Items
    7
    Create draggable items that know which container they belong to. This is important for proper state management.
    8
    import { useDraggable } from '@dnd-kit/react';
    
    function DraggableItem({ id, container }: { id: string; container: string }) {
      const { ref, isDragging } = useDraggable({ 
        id,
        data: { container } // Store container info for later use
      });
    
      return (
        <div 
          ref={ref} 
          className="item"
          style={{ opacity: isDragging ? 0.5 : 1 }}
        >
          {id}
        </div>
      );
    }
    
    9
    Key concepts:
    10
  • Store the container ID in the data object
  • Use isDragging to provide visual feedback
  • The item ID should be unique across all containers
  • 11
    Create Container Components
    12
    Each container acts as both a drop zone and a list of items.
    13
    import { useDroppable } from '@dnd-kit/react';
    
    function Container({ 
      id, 
      items 
    }: { 
      id: string; 
      items: string[];
    }) {
      const { ref, isDropTarget } = useDroppable({ id });
    
      return (
        <div 
          ref={ref} 
          className={`container ${isDropTarget ? 'active' : ''}`}
        >
          <h3>Container {id}</h3>
          <div className="items">
            {items.map((itemId) => (
              <DraggableItem 
                key={itemId} 
                id={itemId} 
                container={id}
              />
            ))}
          </div>
        </div>
      );
    }
    
    14
    Key concepts:
    15
  • Each container is a droppable zone
  • Items are rendered from the container’s item array
  • Visual feedback shows when a container is a valid drop target
  • 16
    Handle Cross-Container Moves
    17
    Update your state when items are dropped into different containers.
    18
    import { DragDropProvider } from '@dnd-kit/react';
    
    function App() {
      const [items, setItems] = useState({
        A: ['A1', 'A2', 'A3'],
        B: ['B1', 'B2', 'B3'],
        C: ['C1', 'C2', 'C3'],
        D: []
      });
    
      return (
        <DragDropProvider
          onDragEnd={(event) => {
            if (event.canceled) return;
    
            const { source, target } = event.operation;
            if (!source || !target) return;
    
            const sourceContainer = source.data?.container as string;
            const targetContainer = target.id as string;
    
            setItems((prev) => {
              const newItems = { ...prev };
              
              // Remove from source container
              newItems[sourceContainer] = newItems[sourceContainer].filter(
                (id) => id !== source.id
              );
              
              // Add to target container
              newItems[targetContainer] = [
                ...newItems[targetContainer],
                source.id as string
              ];
              
              return newItems;
            });
          }}
        >
          <div className="grid">
            {Object.entries(items).map(([containerId, containerItems]) => (
              <Container 
                key={containerId} 
                id={containerId} 
                items={containerItems}
              />
            ))}
          </div>
        </DragDropProvider>
      );
    }
    
    19
    Key concepts:
    20
  • Access source container from source.data.container
  • Target container is target.id
  • Remove item from source array
  • Add item to target array
  • Use functional state updates for reliability
  • Complete Example

    Here’s a full working implementation with multiple containers:
    import React, { useState } from 'react';
    import { DragDropProvider, useDraggable, useDroppable } from '@dnd-kit/react';
    
    function Draggable({ id }: { id: string }) {
      const { ref } = useDraggable({ id });
      return <button ref={ref} className="btn">draggable</button>;
    }
    
    function Droppable({ id, children }: { id: string; children?: React.ReactNode }) {
      const { ref, isDropTarget } = useDroppable({ id });
      
      return (
        <div ref={ref} className={isDropTarget ? "droppable active" : "droppable"}>
          <h3>Container {id}</h3>
          {children}
        </div>
      );
    }
    
    export default function App() {
      const [parent, setParent] = useState<string | undefined>(undefined);
      const draggable = <Draggable id="draggable" />;
      const droppables = ['A', 'B', 'C'];
    
      return (
        <DragDropProvider
          onDragEnd={(event) => {
            if (event.canceled) return;
            setParent(event.operation.target?.id as string);
          }}
        >
          <div className="grid">
            <div className="start-zone">
              {parent == null ? draggable : null}
            </div>
            {droppables.map((id) => (
              <Droppable key={id} id={id}>
                {parent === id ? draggable : null}
              </Droppable>
            ))}
          </div>
        </DragDropProvider>
      );
    }
    

    Styling

    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 20px;
      padding: 40px;
      max-width: 900px;
      margin: 0 auto;
    }
    
    .container {
      min-height: 200px;
      padding: 16px;
      border: 2px solid #e0e0e0;
      border-radius: 8px;
      background: white;
      transition: all 0.2s;
    }
    
    .container.active {
      border-color: #0070f3;
      background: #f0f7ff;
      box-shadow: 0 4px 12px rgba(0, 112, 243, 0.1);
    }
    
    .container h3 {
      margin: 0 0 12px 0;
      font-size: 14px;
      font-weight: 600;
      color: #666;
      text-transform: uppercase;
    }
    
    .items {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    
    .item {
      padding: 12px;
      background: #0070f3;
      color: white;
      border-radius: 6px;
      cursor: grab;
      user-select: none;
      transition: opacity 0.2s;
    }
    
    .item:active {
      cursor: grabbing;
    }
    
    .start-zone {
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 200px;
      padding: 16px;
      border: 2px dashed #ccc;
      border-radius: 8px;
      background: #fafafa;
    }
    

    Advanced: Using the Move Helper

    For sortable lists within containers, dnd-kit provides a move helper from @dnd-kit/helpers:
    import { move } from '@dnd-kit/helpers';
    import { useSortable } from '@dnd-kit/react/sortable';
    
    // In your onDragEnd handler:
    onDragEnd={(event) => {
      if (event.canceled) return;
      
      setItems((items) => move(items, event));
    }}
    
    This automatically handles moving items between containers and reordering within containers.

    Key Takeaways

    • Structure your state as an object mapping container IDs to item arrays
    • Store container information in the draggable’s data property
    • Use event.operation.source and event.operation.target to identify containers
    • Remove items from the source container and add to the target container
    • The move helper from @dnd-kit/helpers can simplify state updates
    • Visual feedback helps users understand where items can be dropped

    Common Patterns

    Preventing Drops in Certain Containers

    onDragEnd={(event) => {
      if (event.canceled) return;
      
      const targetContainer = event.operation.target?.id;
      
      // Don't allow drops in container D
      if (targetContainer === 'D') return;
      
      // Continue with state update
    }}
    

    Limiting Items Per Container

    onDragEnd={(event) => {
      const targetContainer = event.operation.target?.id as string;
      const targetItems = items[targetContainer];
      
      // Max 5 items per container
      if (targetItems.length >= 5) {
        return; // Don't update state
      }
      
      // Continue with state update
    }}
    

    Next Steps

    • Explore Nested Contexts for hierarchical structures
    • Learn about sortable lists for ordering within containers
    • Add animations with drag overlays for smoother interactions

    Build docs developers (and LLMs) love