BlogReactJS

Creating a Trello-like drag and drop kanban board in React

Written by Codemzy on October 20th, 2023

One of my favourite features of Trello is how you can drag and drop your cards around, so here's how I created a Trello-like drag-and-drop kanban board from scratch in React.

In the last blog post, I built a reusable drag-and-drop component in React. I called it Drag. To test it out, I recreated one of my favourite drag-and-drop experiences - Trello!

Here's what we will build:

See the Pen Trello-style React Drag and Drop Lists by Codemzy (@codemzy) on CodePen.

You'll need to grab the code for the Drag component to follow along with this tutorial.

The data

Trello boards are primarily lists and cards.

Since we are focusing on the drag-and-drop features of Trello (and no recreating a real Trello clone), let's start with some simple dummy data to represent our lists and cards.

// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

Our dummy data is an array, and each item is an object that represents a list.

Each list has a cards property that is also an array. Each item in that array is an object that represents a card.

The design

Let's start by making a board that looks like Trello.

I'm using Tailwind CSS to style my Trello-like board. I use Tailwind for most of my projects and find it helps me style things quickly without writing a bunch of custom CSS.

If you prefer not to use Tailwind CSS, you can get all the styles for the classes I use in this tutorial from the Tailwind CSS docs.

Here's the board:

<div id="app"></div>
// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

function Card({ title, description = "Drag and drop me!" }) {
  return (
    <div className="rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2">
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

function List({ name, children }) {
  return (
    <div className="rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow">
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

// app component
function App() {
  const [data, setData] = React.useState(dummyData);
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <div className="flex items-start -mx-2 overflow-x-scroll h-full">
        {data.map((list, listPosition) => {
          return (
            <List key={list.id} name={list.name}>
              {data[listPosition].cards.map((card) => {
                return (
                  <Card key={card.id} title={card.title} />
                );
              })}
            </List>
          );
        })}
      </div>
    </div>
  );
};


// ========================================
// render the react app
ReactDOM.render(
 <App />,
 document.getElementById('app')
);

Again, we'll focus on the drag-and-drop features, but I'll quickly discuss the code for this UI before we start dragging and dropping.

Card Component

import React from 'react';

function Card({ title, description = "Drag and drop me!" }) {
  return (
    <div className="rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2">
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

export default Card;

The card is rounded and white, with a border and a small shadow. It takes a title prop (from our data) and a description - which I've just given a default string for now.

List

import React from 'react';

function List({ name, children }) {
  return (
    <div className="rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow">
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

export default List;

The list is rounded and grey with a small shadow, and the list name is displayed at the top - just like Trello!

App

import React from 'react';

function App() {
  const [data, setData] = React.useState(dummyData);
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <div className="flex items-start -mx-2 overflow-x-scroll h-full">
        {data.map((list, listPosition) => {
          return (
            <List key={list.id} name={list.name}>
              {data[listPosition].cards.map((card) => {
                return (
                  <Card key={card.id} title={card.title} />
                );
              })}
            </List>
          );
        })}
      </div>
    </div>
  );
};

export default App;

The App component is our board view. Our board contains our lists and cards.

We loop over our array of objects to display each list and card using the Array.map() function.

Starting with the lists, we map the data array and return a List for each item. Then, inside each list, we map the cards array and return a Card for each item.

I think it looks Trello-like! Let's move on to adding the drag-and-drop features.

Dragging cards

For this part, you need the Drag component we coded in the last blog post. The code for Drag is available on Github to download if you want to skip the details!

Let's make our cards draggable!

import React from 'react';
import Drag from './Drag'; // import the Drag component

// app component
function App() {
  const [data, setData] = React.useState(dummyData);

  // handle a dropped item (TO DO)
  function handleDrop({ dragItem, dragType, drop }) {
    // do something
  };
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <Drag handleDrop={handleDrop}>
        <div className="flex items-start -mx-2 overflow-x-scroll h-full">
          {data.map((list, listPosition) => {
            return (
              <List key={list.id} name={list.name}>
                {data[listPosition].cards.map((card) => {
                  return (
                    <Drag.DragItem key={card.id} dragId={card.id} dragType="card">
                      <Card title={card.title} />
                    </Drag.DragItem>
                  );
                })}
              </List>
            );
          })}
        </div>
      </Drag>
    </div>
  );
};

export default App;

Okay, a few things have happened here.

We have imported the Drag component (that we built earlier).

import Drag from './Drag';

We have wrapped our list area in the Drag component and our Cards in Drag.DragItem (to make them draggable!).

The DragItem takes a dragId so we know which item is getting dragged - this will be important later when we need to handle the drop. Each of our cards has a unique ID (card.id), and we can use that.

return (
  <Drag.DragItem key={card.id} dragId={card.id} dragType="card">
    <Card title={card.title} />
  </Drag.DragItem>
);

We should also add the "select-none" class to the List component to prevent selecting text on the lists or cards - because that will interfere with our drag-and-drop.

<div className="rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow select-none">

Now we can drag our cards around - we can't drop them yet - but we will fix that shortly.

<div id="app"></div>
// context for the drag
const DragContext = React.createContext();

// drag context component
function Drag({ draggable = true, handleDrop, children }) {
  const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
  const [dragItem, setDragItem] = React.useState(null); // the item being dragged
  const [isDragging, setIsDragging] = React.useState(null);
  const [drop, setDrop] = React.useState(null); // the active dropzone
  
  React.useEffect(() => {
    if (dragItem) {
      document.body.style.cursor = "grabbing";
    } else {
      document.body.style.cursor = "default";
    }
  }, [dragItem]);
  
  const dragStart = function(e, dragId, dragType) {
    e.stopPropagation();
    e.dataTransfer.effectAllowed = 'move';
    setDragItem(dragId);
    setDragType(dragType);
  };
  
  const drag = function(e, dragId, dragType) {
    e.stopPropagation();
    setIsDragging(true);
  };
  
  const dragEnd = function(e) {
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  const onDrop = function(e) {
    e.preventDefault();
    handleDrop({ dragItem, dragType, drop });
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  return (
    <DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
      { typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
    </DragContext.Provider>
  );
};

// a draggable item
Drag.DragItem = function({ as, dragId, dragType, ...props }) {
  const { draggable, dragStart, drag, dragEnd, dragItem } = React.useContext(DragContext);
  
  let Component = as || "div";
  return <Component onDragStart={(e) => dragStart(e, dragId, dragType)} onDrag={drag} draggable={draggable} onDragEnd={dragEnd} {...props} />;
};

// listens for drags over drop zones
Drag.DropZone = function({ as, dropId, dropType, remember, children, style, ...props }) {
  const { dragItem, dragType, setDrop, drop, onDrop } = React.useContext(DragContext);
  
  function handleDragOver(e) {
    if (e.preventDefault) {
      e.preventDefault();
    }
    return false;
  };
  
  function handleLeave() {
    if (!remember) {
      setDrop(null); 
    }
  };
  
  let Component = as || "div";
  return ( 
    <Component onDragEnter={(e) => dragItem && dropType === dragType && setDrop(dropId)} onDragOver={handleDragOver} onDrop={onDrop} style={{position: "relative", ...style}} {...props}>
      { children }
      { drop === dropId && <div style={{position: "absolute", inset: "0px"}} onDragLeave={handleLeave}></div> }
    </Component>
  );
};

// if we need multiple dropzones
Drag.DropZones = function({ dropType, prevId, nextId, split = "y", remember, children }) {
  const { dragType, isDragging } = React.useContext(DragContext);
  return (
    <div style={{position: "relative"}}>
      { children }
      { dragType === dropType && isDragging &&
        <div style={{position: "absolute", inset: "0px" }}>
          <Drag.DropZone dropId={prevId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
          <Drag.DropZone dropId={nextId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
        </div>
      }
    </div>
  );
};

// indicates where the drop will go when dragging over a dropzone
Drag.DropGuide = function({ as, dropId, dropType, ...props }) {
    const { drop, dragType } = React.useContext(DragContext);
    let Component = as || "div";
    return dragType === dropType && drop === dropId ? <Component {...props} /> : null;
};

// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

function Card({ title, description = "Drag and drop me!" }) {
  return (
    <div className="rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2">
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

function List({ name, children }) {
  return (
    <div className="rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow select-none">
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

// app component
function App() {
  const [data, setData] = React.useState(dummyData);
  
  function handleDrop({ dragItem, dragType, drop }) {
    // do something
  };
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <Drag handleDrop={handleDrop}>
        <div className="flex items-start -mx-2 overflow-x-scroll h-full">
          {data.map((list, listPosition) => {
            return (
              <List key={list.id} name={list.name}>
                {data[listPosition].cards.map((card) => {
                  return (
                    <Drag.DragItem key={card.id} dragId={card.id} dragType="card">
                      <Card title={card.title} />
                    </Drag.DragItem>
                  );
                })}
              </List>
            );
          })}
        </div>
      </Drag>
    </div>
  );
};


// ========================================
// render the react app
ReactDOM.render(
 <App />,
 document.getElementById('app')
);

First, there are a couple of problems.

  • In Trello cards disappear from the list during a drag, but ours stays on the page
  • Our dragged card includes part of the background and doesn't have rounded corners

Hiding the original element

Usually, when I do a drag and drop, I leave the item I am dragging in the DOM but fade it out (by reducing the opacity).

Trello doesn't do this. Trello hides the orginal element (the card) while it gets dragged, and only shows it as the drag image.

Let's make that happen!

Drag provides us with three render props we can use, activeItem, activeType, and isDragging.

<Drag handleDrop={handleDrop}>
  {({ activeItem, activeType, isDragging }) => ( // 🆕 render props
    <div className="flex items-start -mx-2 overflow-x-scroll h-full">
      {data.map((list, listPosition) => {
        return (
          <List key={list.id} name={list.name}>
            {data[listPosition].cards.map((card) => {
              return (
                <Drag.DragItem key={card.id} dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" ? "hidden" : ""}`} dragType="card">
                  <Card title={card.title} />
                </Drag.DragItem>
              );
            })}
          </List>
        );
      })}
    </div>
  )}
</Drag>

Now I can check if the render props match the card, and if they do, add the "hidden" class to hide it from view. I've also added the "cursor-pointer" class to change the cursor and provide a little clue that it is interactive.

className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : ""}`}

My Drag component handles hidden elements (now), but it was tricky to implement, so let me explain how I fixed it.

At first, I changed the styles of my dragged items on the "dragstart" event. But because the drag image gets created by the browser at the end of the dragStart event, any styles got picked up in the drag image.

That means if I hide a dragged item, it will also get hidden in the drag image! Whoops!

The styles needed to be set after the "dragstart" event instead.

But to add to the complexity, I do need to change some styles on the "dragstart" event, because Trello rotates the drag image card slightly so it's at a cool jaunty angle when you drag it.

In my Drag component, I added a isDragging state. This is only true once the drag event is happening (using the onDrag event instead of onDragStart). That means the isDragging render prop comes after the drag has started and I can safely hide the card once that is true.

Giving the drag element rounded corners

Another little bug that had me stumped was the drag image. My cards have rounded corners (like Trello). But when the drag image (created in the browser during a drag) has square corners.

And that means you can see the background in the corners of the drag image.

That does not look good - I need a fix!

Luckily, I found a pretty weird solution in this GitHub issue - use "translate-x-0".

<Drag.DragItem key={card.id} dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">

With that style added, the rounded corners don't get picked up in the drag image - sweet!

Rotating the drag image

When you drag an item, the browser will screenshot the DOM node being dragged. And that's native HTML5 behaviour that works out the box.

But when you drag a card in Trello, the drag image gets rotated slightly - which I think looks pretty neat!

We want to add this style when the drag event starts, but before our original element gets hidden.

So we will rotate our card when the activeItem is set (before isDragging). Then our card will rotate and the drag image will be created before the original element is hidden (the code we just did).

And another little tip - we need to add this style inside the drag element - adding it the DragItem won't work, we need to rotate an element inside it. So let's rotate the Card component instead.

Here's how the code looks:

<Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />

Now the Card element knows when it is the drag item.

function Card({ title, description = "Drag and drop me!", dragItem }) {
  return (
    <div className={`rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2${ dragItem ? " rotate-6" : ""}`}>
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

And when it's the drag item, it rotates!

<div id="app"></div>
// context for the drag
const DragContext = React.createContext();

// drag context component
function Drag({ draggable = true, handleDrop, children }) {
  const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
  const [dragItem, setDragItem] = React.useState(null); // the item being dragged
  const [isDragging, setIsDragging] = React.useState(null);
  const [drop, setDrop] = React.useState(null); // the active dropzone
  
  React.useEffect(() => {
    if (dragItem) {
      document.body.style.cursor = "grabbing";
    } else {
      document.body.style.cursor = "default";
    }
  }, [dragItem]);
  
  const dragStart = function(e, dragId, dragType) {
    e.stopPropagation();
    e.dataTransfer.effectAllowed = 'move';
    setDragItem(dragId);
    setDragType(dragType);
  };
  
  const drag = function(e, dragId, dragType) {
    e.stopPropagation();
    setIsDragging(true);
  };
  
  const dragEnd = function(e) {
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  const onDrop = function(e) {
    e.preventDefault();
    handleDrop({ dragItem, dragType, drop });
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  return (
    <DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
      { typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
    </DragContext.Provider>
  );
};

// a draggable item
Drag.DragItem = function({ as, dragId, dragType, ...props }) {
  const { draggable, dragStart, drag, dragEnd, dragItem } = React.useContext(DragContext);
  
  let Component = as || "div";
  return <Component onDragStart={(e) => dragStart(e, dragId, dragType)} onDrag={drag} draggable={draggable} onDragEnd={dragEnd} {...props} />;
};

// listens for drags over drop zones
Drag.DropZone = function({ as, dropId, dropType, remember, children, style, ...props }) {
  const { dragItem, dragType, setDrop, drop, onDrop } = React.useContext(DragContext);
  
  function handleDragOver(e) {
    if (e.preventDefault) {
      e.preventDefault();
    }
    return false;
  };
  
  function handleLeave() {
    if (!remember) {
      setDrop(null); 
    }
  };
  
  let Component = as || "div";
  return ( 
    <Component onDragEnter={(e) => dragItem && dropType === dragType && setDrop(dropId)} onDragOver={handleDragOver} onDrop={onDrop} style={{position: "relative", ...style}} {...props}>
      { children }
      { drop === dropId && <div style={{position: "absolute", inset: "0px"}} onDragLeave={handleLeave}></div> }
    </Component>
  );
};

// if we need multiple dropzones
Drag.DropZones = function({ dropType, prevId, nextId, split = "y", remember, children }) {
  const { dragType, isDragging } = React.useContext(DragContext);
  return (
    <div style={{position: "relative"}}>
      { children }
      { dragType === dropType && isDragging &&
        <div style={{position: "absolute", inset: "0px" }}>
          <Drag.DropZone dropId={prevId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
          <Drag.DropZone dropId={nextId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
        </div>
      }
    </div>
  );
};

// indicates where the drop will go when dragging over a dropzone
Drag.DropGuide = function({ as, dropId, dropType, ...props }) {
    const { drop, dragType } = React.useContext(DragContext);
    let Component = as || "div";
    return dragType === dropType && drop === dropId ? <Component {...props} /> : null;
};

// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

function Card({ title, description = "Drag and drop me!", dragItem }) {
  return (
    <div className={`rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2${ dragItem ? " rotate-6" : ""}`}>
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

function List({ name, children }) {
  return (
    <div className="rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow select-none">
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

// app component
function App() {
  const [data, setData] = React.useState(dummyData);
  
  // handle a dropped item (TO DO)
  function handleDrop({ dragItem, dragType, drop }) {
    // do something
  };
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <Drag handleDrop={handleDrop}>
        {({ activeItem, activeType, isDragging }) => (
          <div className="flex items-start -mx-2 overflow-x-scroll h-full">
            {data.map((list, listPosition) => {
              return (
                <List key={list.id} name={list.name}>
                  {data[listPosition].cards.map((card) => {
                    return (
                      <Drag.DragItem key={card.id} dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
                        <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
                      </Drag.DragItem>
                    );
                  })}
                </List>
              );
            })}
          </div>
        )}
      </Drag>
    </div>
  );
};


// ========================================
// render the react app
ReactDOM.render(
 <App />,
 document.getElementById('app')
);

Dropping cards

We can drag our cards, but we can't drop them yet. To drop cards, we need some drop zones!

How Trello handles drop zones is different because the drop zones aren't empty boxes that you drop things in. As you drag a card around, drop zones appear.

Here's how I think of it:

  • each card is a drop zone (letting you drop a card above or below it)
  • the end of each list is a drop zone (letting you drop a card at the end of the list)
  • the board is a drop zone (handling the last drop zone you triggered)

Each card is a drop zone

Let's start with drop zones around cards.

{data[listPosition].cards.map((card, cardPosition) => {
  return (
    <Drag.DropZone key={card.id} dropId={`${listPosition}-${cardPosition}`} dropType="card">
      <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
        <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
      </Drag.DragItem>
    </Drag.DropZone>
  );
})}

I pass the Drag.DropZone a dropId, to let me know which drop zone is active. If you have only one list, this ID could simply be the cardPosition. However, we are going to have drop zones in multiple lists, so my ID is ${listPosition}-${cardPosition} so that I know both:

  • which list the user wants to drop the card in
  • the position in that list

Great - our drop zone is in place.

Adding DropGuides

Our DropZones are wrappers around our cards, but when we drag an item, we don't want it to replace the card. We want the dragged card placed above or below the drop zone.

That's why I created the Drag.DropGuide component.

The DropGuide says "if you let go, here's where you will land".

In Trello, the DropGuide is a grey box that fills the space where the dragged card will go if you drop it.

Let's add a DropGuide so that you can see what I mean.

return (
  <Drag.DropZone key={card.id} dropId={`${listPosition}-${cardPosition}`} dropType="card">
    <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
    <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
      <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
    </Drag.DragItem>
  </Drag.DropZone>
);

The Drag.DropGuide takes a dropId and whenever that dropId is active the Drag.DropGuide will display. So in this case, the dropId matches the one for the Drag.DropZone - when the drop zone is active, the drop guide will display.

If you're coding along, you might notice that the guide disappears when you move off a drop zone. But in Trello, you can move away from a drop zone, and it remembers the last active drop zone.

I added a remember prop to my drop zones so that this will work.

<Drag.DropZone ... remember={true}>

The end of the list is a drop zone

In Trello, if you drag a card at the bottom of the list (or below a list), it activates the drop zone at the bottom of that list.

To do this, I need two drop zones. And another drop guide.

All my other drop guides are above each card, but if we want to drag a card to the bottom of a list, we need a guide there too. We can only drop a card in a drop zone, so if we drop a card on that guide, it needs to be in a drop zone to handle the drop.

Let's do this!

<List key={list.id} name={list.name}>
  {data[listPosition].cards.map((card, cardPosition) => {
    return (
      <Drag.DropZone key={card.id} dropId={`${listPosition}-${cardPosition}`} dropType="card" remember={true}>
        <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
        <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
          <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
        </Drag.DragItem>
      </Drag.DropZone>
    );
  })}
  {/* 🆕 */}
  <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} dropType="card" remember={true}>
    <Drag.DropGuide dropId={`${listPosition}-${data[listPosition].cards.length}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
  </Drag.DropZone>
</List>

This drop zone activates the guide at the bottom of the list, so the ID is ${listPosition}-${data[listPosition].cards.length} to be the last item in the array.

We also need a drop zone below the list. To get this to work Trello-style, I'll wrap each list in a "flex" div and have an invisible DropZone at the bottom to listen for drags under the list.

<Drag handleDrop={handleDrop}>
  {({ activeItem, activeType }) => (
    <div className="flex items-start -mx-2 overflow-x-scroll h-full">
      {data.map((list, listPosition) => {
        return (
          <div key={list.id} className="flex flex-col h-full">
            <List name={list.name}>
              {data[listPosition].cards.map((card, cardPosition) => {
                return (
                  <Drag.DropZone key={card.id} dropId={`${listPosition}-${cardPosition}`} dropType="card" remember={true}>
                    <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                    <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
                      <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
                    </Drag.DragItem>
                  </Drag.DropZone>
                );
              })}
              <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} dropType="card" remember={true}>
                <Drag.DropGuide dropId={`${listPosition}-${data[listPosition].cards.length}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
              </Drag.DropZone>
            </List>
            <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} className="grow" dropType="card" remember={true} />
          </div>
        );
      })}
    </div>
  )}
</Drag>

This bottom drop zone also activates the last drop zone on the list (the one we created earlier).

<div id="app"></div>
// context for the drag
const DragContext = React.createContext();

// drag context component
function Drag({ draggable = true, handleDrop, children }) {
  const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
  const [dragItem, setDragItem] = React.useState(null); // the item being dragged
  const [isDragging, setIsDragging] = React.useState(null);
  const [drop, setDrop] = React.useState(null); // the active dropzone
  
  React.useEffect(() => {
    if (dragItem) {
      document.body.style.cursor = "grabbing";
    } else {
      document.body.style.cursor = "default";
    }
  }, [dragItem]);
  
  const dragStart = function(e, dragId, dragType) {
    e.stopPropagation();
    e.dataTransfer.effectAllowed = 'move';
    setDragItem(dragId);
    setDragType(dragType);
  };
  
  const drag = function(e, dragId, dragType) {
    e.stopPropagation();
    setIsDragging(true);
  };
  
  const dragEnd = function(e) {
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  const onDrop = function(e) {
    e.preventDefault();
    handleDrop({ dragItem, dragType, drop });
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  return (
    <DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
      { typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
    </DragContext.Provider>
  );
};

// a draggable item
Drag.DragItem = function({ as, dragId, dragType, ...props }) {
  const { draggable, dragStart, drag, dragEnd, dragItem } = React.useContext(DragContext);
  
  let Component = as || "div";
  return <Component onDragStart={(e) => dragStart(e, dragId, dragType)} onDrag={drag} draggable={draggable} onDragEnd={dragEnd} {...props} />;
};

// listens for drags over drop zones
Drag.DropZone = function({ as, dropId, dropType, remember, children, style, ...props }) {
  const { dragItem, dragType, setDrop, drop, onDrop } = React.useContext(DragContext);
  
  function handleDragOver(e) {
    if (e.preventDefault) {
      e.preventDefault();
    }
    return false;
  };
  
  function handleLeave() {
    if (!remember) {
      setDrop(null); 
    }
  };
  
  let Component = as || "div";
  return ( 
    <Component onDragEnter={(e) => dragItem && dropType === dragType && setDrop(dropId)} onDragOver={handleDragOver} onDrop={onDrop} style={{position: "relative", ...style}} {...props}>
      { children }
      { drop === dropId && <div style={{position: "absolute", inset: "0px"}} onDragLeave={handleLeave}></div> }
    </Component>
  );
};

// if we need multiple dropzones
Drag.DropZones = function({ dropType, prevId, nextId, split = "y", remember, children }) {
  const { dragType, isDragging } = React.useContext(DragContext);
  return (
    <div style={{position: "relative"}}>
      { children }
      { dragType === dropType && isDragging &&
        <div style={{position: "absolute", inset: "0px" }}>
          <Drag.DropZone dropId={prevId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
          <Drag.DropZone dropId={nextId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
        </div>
      }
    </div>
  );
};

// indicates where the drop will go when dragging over a dropzone
Drag.DropGuide = function({ as, dropId, dropType, ...props }) {
    const { drop, dragType } = React.useContext(DragContext);
    let Component = as || "div";
    return dragType === dropType && drop === dropId ? <Component {...props} /> : null;
};

// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

function Card({ title, description = "Drag and drop me!", dragItem }) {
  return (
    <div className={`rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2${ dragItem ? " rotate-6" : ""}`}>
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

function List({ name, children }) {
  return (
    <div className="rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow select-none">
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

// app component
function App() {
  const [data, setData] = React.useState(dummyData);
  
  // handle a dropped item (TO DO)
  function handleDrop({ dragItem, dragType, drop }) {
    // do something
  };
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <Drag handleDrop={handleDrop}>
        {({ activeItem, activeType, isDragging }) => (
          <div className="flex items-start -mx-2 overflow-x-scroll h-full">
            {data.map((list, listPosition) => {
              return (
                <div key={list.id} className="flex flex-col h-full">
                  <List name={list.name}>
                    {data[listPosition].cards.map((card, cardPosition) => {
                      return (
                        <Drag.DropZone key={card.id} dropId={`${listPosition}-${cardPosition}`} dropType="card" remember={true}>
                          <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                          <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
                            <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
                          </Drag.DragItem>
                        </Drag.DropZone>
                      );
                    })}
                    <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} dropType="card" remember={true}>
                      <Drag.DropGuide dropId={`${listPosition}-${data[listPosition].cards.length}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                    </Drag.DropZone>
                  </List>
                  <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} className="grow" dropType="card" remember={true} />
                </div>
              );
            })}
          </div>
        )}
      </Drag>
    </div>
  );
};


// ========================================
// render the react app
ReactDOM.render(
 <App />,
 document.getElementById('app')
);

The board is a drop zone

If you don't drop your card on a drop zone, it goes back to where it started. And that's how drag-and-drop works.

But in Trello, if you drop a card somewhere on the board, it gets dropped to the last drop zone you activated.

We need to make our board a drop zone!

This drop zone is a little different. It will have no dropId.

And the reason for no dropId? We only want to catch drop events but keep the last drop zone in the lists the user activated.

So let's turn the first div inside our <Drag> into a DropZone.

From this:

<div className="flex items-start -mx-2 overflow-x-scroll h-full">

To this:

<Drag.DropZone className="flex -mx-2 overflow-x-scroll h-full">

Fixing the card drop zones

Okay, so I've made each card into a drop zone, and when you drag over a drop zone, the DropGuide shows where it will move.

It’s working ok. If we drag an item over a drop zone, it sets drop to that index, and the drop guide displays above that card, which is great but when we drag cards down, the drop position lags behind us

Dragging cards up looks great. Dragging cards down looks a bit off.

When I compare this with Trello, it looks like when a card drags in from the top, it gets positioned after the next card (instead of before it).

We can easily change this with setDrop(dropId + 1), but we would have the opposite problem. Cards dragged down would look great, but dragging up would lag.

I went through a bunch of ideas for how I could get this to work better.

First, I tried checking what direction the mouse was dragging (like my useScrollDirection hook but for the "drag" event), and setting the position after the drop zone if it was moving downwards. This sort of worked - but not if the user changed direction while over a drop zone.

Then I tried using the onDrag instead, but because the drop zones would jump about as you drag a card from one position to the next, it was hard to get a smooth experience.

And all the extra event listeners and debouncing I needed to track drag events and mouse movements were just adding complexity.

So I came up with a better idea...

Two drop zones per card!

Because two drop zones are better than one (in this case!).

Instead of one drop zone, I need my card to have two drop zones. One for the top half and one for the bottom half.

If you drag over the top half of the drop zone the drop position will be i - placing it above the drop zone.

And if you drag into the bottom half, the drop position will be i+1 - placing it below the drop zone.

I feel like I'm going to need this double-drop zone feature whenever I build a drag-and-drop list like this, so I bundled this up into a DropZones sub-component and added it to my Drag component.

I want to split my cards in half by height, which is the default split, so all I need to do is change DropZone to DropZones and give two new props. prevId which will be dropId we already had. And nextId where we will add 1 to the card position ${listPosition}-${cardPosition+1}, activating the drop zone after the card (instead of before it).

{data[listPosition].cards.map((card, cardPosition) => {
  return (
    <Drag.DropZones key={card.id} prevId={`${listPosition}-${cardPosition}`} nextId={`${listPosition}-${cardPosition+1}`} dropType="card" remember={true}>
      <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
      <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
        <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
      </Drag.DragItem>
    </Drag.DropZones>
  );
})}

We are only changing the DropZone around the cards to this double DropZones component. Our other DropZones will stay as they are.

Now our drag-and-drop feels much more responsive.

<div id="app"></div>
// context for the drag
const DragContext = React.createContext();

// drag context component
function Drag({ draggable = true, handleDrop, children }) {
  const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
  const [dragItem, setDragItem] = React.useState(null); // the item being dragged
  const [isDragging, setIsDragging] = React.useState(null);
  const [drop, setDrop] = React.useState(null); // the active dropzone
  
  React.useEffect(() => {
    if (dragItem) {
      document.body.style.cursor = "grabbing";
    } else {
      document.body.style.cursor = "default";
    }
  }, [dragItem]);
  
  const dragStart = function(e, dragId, dragType) {
    e.stopPropagation();
    e.dataTransfer.effectAllowed = 'move';
    setDragItem(dragId);
    setDragType(dragType);
  };
  
  const drag = function(e, dragId, dragType) {
    e.stopPropagation();
    setIsDragging(true);
  };
  
  const dragEnd = function(e) {
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  const onDrop = function(e) {
    e.preventDefault();
    handleDrop({ dragItem, dragType, drop });
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  return (
    <DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
      { typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
    </DragContext.Provider>
  );
};

// a draggable item
Drag.DragItem = function({ as, dragId, dragType, ...props }) {
  const { draggable, dragStart, drag, dragEnd, dragItem } = React.useContext(DragContext);
  
  let Component = as || "div";
  return <Component onDragStart={(e) => dragStart(e, dragId, dragType)} onDrag={drag} draggable={draggable} onDragEnd={dragEnd} {...props} />;
};

// listens for drags over drop zones
Drag.DropZone = function({ as, dropId, dropType, remember, children, style, ...props }) {
  const { dragItem, dragType, setDrop, drop, onDrop } = React.useContext(DragContext);
  
  function handleDragOver(e) {
    if (e.preventDefault) {
      e.preventDefault();
    }
    return false;
  };
  
  function handleLeave() {
    if (!remember) {
      setDrop(null); 
    }
  };
  
  let Component = as || "div";
  return ( 
    <Component onDragEnter={(e) => dragItem && dropType === dragType && setDrop(dropId)} onDragOver={handleDragOver} onDrop={onDrop} style={{position: "relative", ...style}} {...props}>
      { children }
      { drop === dropId && <div style={{position: "absolute", inset: "0px"}} onDragLeave={handleLeave}></div> }
    </Component>
  );
};

// if we need multiple dropzones
Drag.DropZones = function({ dropType, prevId, nextId, split = "y", remember, children }) {
  const { dragType, isDragging } = React.useContext(DragContext);
  return (
    <div style={{position: "relative"}}>
      { children }
      { dragType === dropType && isDragging &&
        <div style={{position: "absolute", inset: "0px" }}>
          <Drag.DropZone dropId={prevId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
          <Drag.DropZone dropId={nextId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
        </div>
      }
    </div>
  );
};

// indicates where the drop will go when dragging over a dropzone
Drag.DropGuide = function({ as, dropId, dropType, ...props }) {
    const { drop, dragType } = React.useContext(DragContext);
    let Component = as || "div";
    return dragType === dropType && drop === dropId ? <Component {...props} /> : null;
};

// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

function Card({ title, description = "Drag and drop me!", dragItem }) {
  return (
    <div className={`rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2${ dragItem ? " rotate-6" : ""}`}>
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

function List({ name, children }) {
  return (
    <div className="rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow select-none">
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

// app component
function App() {
  const [data, setData] = React.useState(dummyData);
  
  // handle a dropped item (TO DO)
  function handleDrop({ dragItem, dragType, drop }) {
    // do something
  };
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <Drag handleDrop={handleDrop}>
        {({ activeItem, activeType, isDragging }) => (
          <Drag.DropZone className="flex -mx-2 overflow-x-scroll h-full">
            {data.map((list, listPosition) => {
              return (
                <div key={list.id} className="flex flex-col h-full">
                  <List name={list.name}>
                    {data[listPosition].cards.map((card, cardPosition) => {
                      return (
                        <Drag.DropZones key={card.id} prevId={`${listPosition}-${cardPosition}`} nextId={`${listPosition}-${cardPosition+1}`} dropType="card" remember={true}>
                          <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                          <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
                            <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
                          </Drag.DragItem>
                        </Drag.DropZones>
                      );
                    })}
                    <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} dropType="card" remember={true}>
                      <Drag.DropGuide dropId={`${listPosition}-${data[listPosition].cards.length}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                    </Drag.DropZone>
                  </List>
                  <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} className="grow" dropType="card" remember={true} />
                </div>
              );
            })}
          </Drag.DropZone>
        )}
      </Drag>
    </div>
  );
};


// ========================================
// render the react app
ReactDOM.render(
 <App />,
 document.getElementById('app')
);

Handle the drop

Now we need to handle the drop. Remember that function we started earlier in our App component?

// handle a dropped item (TO DO)
function handleDrop({ dragItem, dragType, drop }) {
  // do something
};

It's time to do something!

We already passed this function to Drag (<Drag handleDrop={handleDrop}>), and it will get called when a drag-and-drop item gets dropped. It's called with three arguments, dragItem, dragType and drop.

We've not added drag-and-drop to our lists yet, but let's handle dragType === "card".

// handle a dropped item
function handleDrop({ dragItem, dragType, drop }) {
  if (dragType === "card") {
    // get the drop position as numbers
    let [newListPosition, newCardPosition] = drop.split("-").map((string) => parseInt(string));
    // create a copy for the new data
    let newData = structuredClone(data); // deep clone
    // find the current positions
    let oldCardPosition;
    let oldListPosition = data.findIndex((list) => {
      oldCardPosition = list.cards.findIndex((card) => card.id === dragItem);
      return oldCardPosition >= 0;
    });
    // get the card
    let card = data[oldListPosition].cards[oldCardPosition];
    // if same array and current position before drop reduce drop position by one
    if (newListPosition === oldListPosition && oldCardPosition < newCardPosition) {
      newCardPosition--; // reduce by one
    }
    // remove the card from the old position
    newData[oldListPosition].cards.splice(oldCardPosition, 1);
    // put it in the new position
    newData[newListPosition].cards.splice(newCardPosition, 0, card);
    // update the state
    setData(newData);
  }
};

I've commented this function to show you how I've done it. You're handleDrop function will depend on the structure of your data, the dragId you assign to your items, and the dropId you give to your drop zones.

In this function, I'm:

  • finding the position of where the card was
  • creating a copy of the state so I can make some changes
  • removing the card from the old location
  • inserting it at the new location
  • updating the state with the new updated copy

And now we can drag-and-drop cards!

<div id="app"></div>
// context for the drag
const DragContext = React.createContext();

// drag context component
function Drag({ draggable = true, handleDrop, children }) {
  const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
  const [dragItem, setDragItem] = React.useState(null); // the item being dragged
  const [isDragging, setIsDragging] = React.useState(null);
  const [drop, setDrop] = React.useState(null); // the active dropzone
  
  React.useEffect(() => {
    if (dragItem) {
      document.body.style.cursor = "grabbing";
    } else {
      document.body.style.cursor = "default";
    }
  }, [dragItem]);
  
  const dragStart = function(e, dragId, dragType) {
    e.stopPropagation();
    e.dataTransfer.effectAllowed = 'move';
    setDragItem(dragId);
    setDragType(dragType);
  };
  
  const drag = function(e, dragId, dragType) {
    e.stopPropagation();
    setIsDragging(true);
  };
  
  const dragEnd = function(e) {
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  const onDrop = function(e) {
    e.preventDefault();
    handleDrop({ dragItem, dragType, drop });
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  return (
    <DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
      { typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
    </DragContext.Provider>
  );
};

// a draggable item
Drag.DragItem = function({ as, dragId, dragType, ...props }) {
  const { draggable, dragStart, drag, dragEnd, dragItem } = React.useContext(DragContext);
  
  let Component = as || "div";
  return <Component onDragStart={(e) => dragStart(e, dragId, dragType)} onDrag={drag} draggable={draggable} onDragEnd={dragEnd} {...props} />;
};

// listens for drags over drop zones
Drag.DropZone = function({ as, dropId, dropType, remember, children, style, ...props }) {
  const { dragItem, dragType, setDrop, drop, onDrop } = React.useContext(DragContext);
  
  function handleDragOver(e) {
    if (e.preventDefault) {
      e.preventDefault();
    }
    return false;
  };
  
  function handleLeave() {
    if (!remember) {
      setDrop(null); 
    }
  };
  
  let Component = as || "div";
  return ( 
    <Component onDragEnter={(e) => dragItem && dropType === dragType && setDrop(dropId)} onDragOver={handleDragOver} onDrop={onDrop} style={{position: "relative", ...style}} {...props}>
      { children }
      { drop === dropId && <div style={{position: "absolute", inset: "0px"}} onDragLeave={handleLeave}></div> }
    </Component>
  );
};

// if we need multiple dropzones
Drag.DropZones = function({ dropType, prevId, nextId, split = "y", remember, children }) {
  const { dragType, isDragging } = React.useContext(DragContext);
  return (
    <div style={{position: "relative"}}>
      { children }
      { dragType === dropType && isDragging &&
        <div style={{position: "absolute", inset: "0px" }}>
          <Drag.DropZone dropId={prevId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
          <Drag.DropZone dropId={nextId} style={ split === "x" ? { width: "50%" } : { height: "50%" }} dropType={dropType} remember={remember} />
        </div>
      }
    </div>
  );
};

// indicates where the drop will go when dragging over a dropzone
Drag.DropGuide = function({ as, dropId, dropType, ...props }) {
    const { drop, dragType } = React.useContext(DragContext);
    let Component = as || "div";
    return dragType === dropType && drop === dropId ? <Component {...props} /> : null;
};

// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

function Card({ title, description = "Drag and drop me!", dragItem }) {
  return (
    <div className={`rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2${ dragItem ? " rotate-6" : ""}`}>
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

function List({ name, children }) {
  return (
    <div className="rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow select-none">
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

// app component
function App() {
  const [data, setData] = React.useState(dummyData);
  
  // handle a dropped item
  function handleDrop({ dragItem, dragType, drop }) {
    if (dragType === "card") {
      // get the drop position as numbers
      let [newListPosition, newCardPosition] = drop.split("-").map((string) => parseInt(string));
      // create a copy for the new data
      let newData = structuredClone(data); // deep clone
      // find the current positions
      let oldCardPosition;
      let oldListPosition = data.findIndex((list) => {
        oldCardPosition = list.cards.findIndex((card) => card.id === dragItem);
        return oldCardPosition >= 0;
      });
      // get the card
      let card = data[oldListPosition].cards[oldCardPosition];
      // if same array and current position before drop reduce drop position by one
      if (newListPosition === oldListPosition && oldCardPosition < newCardPosition) {
        newCardPosition--; // reduce by one
      }
      // remove the card from the old position
      newData[oldListPosition].cards.splice(oldCardPosition, 1);
      // put it in the new position
      newData[newListPosition].cards.splice(newCardPosition, 0, card);
      // update the state
      setData(newData);
    }
  };
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <Drag handleDrop={handleDrop}>
        {({ activeItem, activeType, isDragging }) => (
          <Drag.DropZone className="flex -mx-2 overflow-x-scroll h-full">
            {data.map((list, listPosition) => {
              return (
                <div key={list.id} className="flex flex-col h-full">
                  <List name={list.name}>
                    {data[listPosition].cards.map((card, cardPosition) => {
                      return (
                        <Drag.DropZones key={card.id} prevId={`${listPosition}-${cardPosition}`} nextId={`${listPosition}-${cardPosition+1}`} dropType="card" remember={true}>
                          <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                          <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
                            <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
                          </Drag.DragItem>
                        </Drag.DropZones>
                      );
                    })}
                    <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} dropType="card" remember={true}>
                      <Drag.DropGuide dropId={`${listPosition}-${data[listPosition].cards.length}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                    </Drag.DropZone>
                  </List>
                  <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} className="grow" dropType="card" remember={true} />
                </div>
              );
            })}
          </Drag.DropZone>
        )}
      </Drag>
    </div>
  );
};


// ========================================
// render the react app
ReactDOM.render(
 <App />,
 document.getElementById('app')
);

Yay!

Dragging lists

You can also drag and drop lists in Trello!

To wire this up, we will apply most of what we did for cards to lists.

Let's start by wrapping our List components in a DragItem.

<Drag.DragItem dragId={list.id} className={`cursor-pointer ${activeItem === list.id && activeType === "list" && isDragging ? "hidden" : "translate-x-0"}`} dragType="list">
  <List name={list.name} dragItem={activeItem === list.id && activeType === "list"}>

And we are passing the dragItem prop to the List component, so let's update the List component to accept that - like we did on the Card component.

function List({ name, dragItem, children }) {
  return (
    <div className={`rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow${ dragItem ? " rotate-6" : ""}`}>
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

Nice - we can already drag our lists!

<div id="app"></div>
// context for the drag
const DragContext = React.createContext();

// drag context component
function Drag({ draggable = true, handleDrop, children }) {
  const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
  const [dragItem, setDragItem] = React.useState(null); // the item being dragged
  const [isDragging, setIsDragging] = React.useState(null);
  const [drop, setDrop] = React.useState(null); // the active dropzone
  
  React.useEffect(() => {
    if (dragItem) {
      document.body.style.cursor = "grabbing";
    } else {
      document.body.style.cursor = "default";
    }
  }, [dragItem]);
  
  const dragStart = function(e, dragId, dragType) {
    e.stopPropagation();
    e.dataTransfer.effectAllowed = 'move';
    setDragItem(dragId);
    setDragType(dragType);
  };
  
  const drag = function(e, dragId, dragType) {
    e.stopPropagation();
    setIsDragging(true);
  };
  
  const dragEnd = function(e) {
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  const onDrop = function(e) {
    e.preventDefault();
    handleDrop({ dragItem, dragType, drop });
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  return (
    <DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
      { typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
    </DragContext.Provider>
  );
};

// a draggable item
Drag.DragItem = function({ as, dragId, dragType, ...props }) {
  const { draggable, dragStart, drag, dragEnd, dragItem } = React.useContext(DragContext);
  
  let Component = as || "div";
  return <Component onDragStart={(e) => dragStart(e, dragId, dragType)} onDrag={drag} draggable={draggable} onDragEnd={dragEnd} {...props} />;
};

// listens for drags over drop zones
Drag.DropZone = function({ as, dropId, dropType, remember, children, style, ...props }) {
  const { dragItem, dragType, setDrop, drop, onDrop } = React.useContext(DragContext);
  
  function handleDragOver(e) {
    if (e.preventDefault) {
      e.preventDefault();
    }
    return false;
  };
  
  function handleLeave() {
    if (!remember) {
      setDrop(null); 
    }
  };
  
  let Component = as || "div";
  return ( 
    <Component onDragEnter={(e) => dragItem && dropType === dragType && setDrop(dropId)} onDragOver={handleDragOver} onDrop={onDrop} style={{position: "relative", ...style}} {...props}>
      { children }
      { drop === dropId && <div style={{position: "absolute", inset: "0px"}} onDragLeave={handleLeave}></div> }
    </Component>
  );
};

// if we need multiple dropzones
Drag.DropZones = function({ dropType, prevId, nextId, split = "y", remember, children, ...props }) {
  const { dragType, isDragging } = React.useContext(DragContext);
  return (
    <div style={{position: "relative"}} {...props}>
      { children }
      { dragType === dropType && isDragging &&
        <div style={{position: "absolute", inset: "0px", display: "flex", flexDirection: split === "x" ? "row" : "column" }}>
          <Drag.DropZone dropId={prevId} style={{ width: "100%", height: "100%" }} dropType={dropType} remember={remember} />
          <Drag.DropZone dropId={nextId} style={{ width: "100%", height: "100%" }} dropType={dropType} remember={remember} />
        </div>
      }
    </div>
  );
};

// indicates where the drop will go when dragging over a dropzone
Drag.DropGuide = function({ as, dropId, dropType, ...props }) {
    const { drop, dragType } = React.useContext(DragContext);
    let Component = as || "div";
    return dragType === dropType && drop === dropId ? <Component {...props} /> : null;
};

// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

function Card({ title, description = "Drag and drop me!", dragItem }) {
  return (
    <div className={`rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2${ dragItem ? " rotate-6" : ""}`}>
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

function List({ name, dragItem, children }) {
  return (
    <div className={`rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow${ dragItem ? " rotate-6" : ""}`}>
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

// app component
function App() {
  const [data, setData] = React.useState(dummyData);
  
  // handle a dropped item
  function handleDrop({ dragItem, dragType, drop }) {
    if (dragType === "card") {
      // get the drop position as numbers
      let [newListPosition, newCardPosition] = drop.split("-").map((string) => parseInt(string));
      // create a copy for the new data
      let newData = structuredClone(data); // deep clone
      // find the current positions
      let oldCardPosition;
      let oldListPosition = data.findIndex((list) => {
        oldCardPosition = list.cards.findIndex((card) => card.id === dragItem);
        return oldCardPosition >= 0;
      });
      // get the card
      let card = data[oldListPosition].cards[oldCardPosition];
      // if same array and current position before drop reduce drop position by one
      if (newListPosition === oldListPosition && oldCardPosition < newCardPosition) {
        newCardPosition--; // reduce by one
      }
      // remove the card from the old position
      newData[oldListPosition].cards.splice(oldCardPosition, 1);
      // put it in the new position
      newData[newListPosition].cards.splice(newCardPosition, 0, card);
      // update the state
      setData(newData);
    }
  };
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <Drag handleDrop={handleDrop}>
        {({ activeItem, activeType, isDragging }) => (
          <Drag.DropZone className="flex -mx-2 overflow-x-scroll h-full">
            {data.map((list, listPosition) => {
              return (
                <div key={list.id} className="flex flex-col h-full">
                  <Drag.DragItem dragId={list.id} className={`cursor-pointer ${activeItem === list.id && activeType === "list" && isDragging ? "hidden" : "translate-x-0"}`} dragType="list">
                    <List name={list.name} dragItem={activeItem === list.id && activeType === "list"}>
                      {data[listPosition].cards.map((card, cardPosition) => {
                        return (
                          <Drag.DropZones key={card.id} prevId={`${listPosition}-${cardPosition}`} nextId={`${listPosition}-${cardPosition+1}`} dropType="card" remember={true}>
                            <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                            <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
                              <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
                            </Drag.DragItem>
                          </Drag.DropZones>
                        );
                      })}
                      <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} dropType="card" remember={true}>
                        <Drag.DropGuide dropId={`${listPosition}-${data[listPosition].cards.length}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                      </Drag.DropZone>
                    </List>
                  </Drag.DragItem>
                  <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} className="grow" dropType="card" remember={true} />
                </div>
              );
            })}
          </Drag.DropZone>
        )}
      </Drag>
    </div>
  );
};


// ========================================
// render the react app
ReactDOM.render(
 <App />,
 document.getElementById('app')
);

Dropping lists

Now we need to add some drop zones so that we can drop our lists. Like cards, our lists will be drop zones.

And like cards, our lists will be a double drop zone. The first half will drop the item before the list and the second half will drop the item after the list.

Because lists are dropped side to side (instead of above and below like cards), our DropZones component needs to split on the "x" axis, instead of the default "y". We can pass the split prop for that.

First, turn the div that displays our List elements into a drop zone.

From this:

<div key={list.id} className="flex flex-col h-full">

To this:

<Drag.DropZones key={list.id} className="flex flex-col h-full" prevId={listPosition} nextId={listPosition+1} dropType="list" split="x" remember={true}>

Great!

Adding list drop guides

We need our drop guides to see where our lists will drop.

The DropGuide needs to be outside the DropZones, because we want them to display as children of our main flex container so that they show next to our lists - not above or below our lists.

So we are going to add two DropGuides for lists.

One before the list item.

We need to wrap our List in a React.Fragment so that we can return two items, the list drop zone and the new drop guide.

We will also wrap our drop guides in drop zones (like we did for the cards) so we can drop items on the drop guides.

return (
  <React.Fragment key={list.id}>
    <Drag.DropZone dropId={listPosition} dropType="list" remember={true}>
      <Drag.DropGuide dropId={listPosition} dropType="list" className="rounded-xl bg-gray-200 h-96 mx-2 my-5 w-80 shrink-0 grow-0" />
    </Drag.DropZone>
    <Drag.DropZones className="flex flex-col h-full" prevId={listPosition} nextId={listPosition+1} dropType="list" split="x" remember={true}>
      {/* ... */}
    </Drag.DropZones>
  </React.Fragment>
);

Add another drop zone with a drop guide after the list - for when a list gets dragged to the end of the list.

<Drag handleDrop={handleDrop}>
  {({ activeItem, activeType, isDragging }) => (
    <Drag.DropZone className="flex -mx-2 overflow-x-scroll h-full">
      {data.map((list, listPosition) => {
        return (
          //...
        );
      })}
      <Drag.DropZone dropId={data.length} dropType="list" remember={true}>
        <Drag.DropGuide dropId={data.length} dropType="list" className="rounded-xl bg-gray-200 h-96 mx-2 my-5 w-80 shrink-0 grow-0" />
      </Drag.DropZone>
    </Drag.DropZone>
  )}
</Drag>
<div id="app"></div>
// context for the drag
const DragContext = React.createContext();

// drag context component
function Drag({ draggable = true, handleDrop, children }) {
  const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
  const [dragItem, setDragItem] = React.useState(null); // the item being dragged
  const [isDragging, setIsDragging] = React.useState(null);
  const [drop, setDrop] = React.useState(null); // the active dropzone
  
  React.useEffect(() => {
    if (dragItem) {
      document.body.style.cursor = "grabbing";
    } else {
      document.body.style.cursor = "default";
    }
  }, [dragItem]);
  
  const dragStart = function(e, dragId, dragType) {
    e.stopPropagation();
    e.dataTransfer.effectAllowed = 'move';
    setDragItem(dragId);
    setDragType(dragType);
  };
  
  const drag = function(e, dragId, dragType) {
    e.stopPropagation();
    setIsDragging(true);
  };
  
  const dragEnd = function(e) {
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  const onDrop = function(e) {
    e.preventDefault();
    handleDrop({ dragItem, dragType, drop });
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  return (
    <DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
      { typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
    </DragContext.Provider>
  );
};

// a draggable item
Drag.DragItem = function({ as, dragId, dragType, ...props }) {
  const { draggable, dragStart, drag, dragEnd, dragItem } = React.useContext(DragContext);
  
  let Component = as || "div";
  return <Component onDragStart={(e) => dragStart(e, dragId, dragType)} onDrag={drag} draggable={draggable} onDragEnd={dragEnd} {...props} />;
};

// listens for drags over drop zones
Drag.DropZone = function({ as, dropId, dropType, remember, children, style, ...props }) {
  const { dragItem, dragType, setDrop, drop, onDrop } = React.useContext(DragContext);
  
  function handleDragOver(e) {
    if (e.preventDefault) {
      e.preventDefault();
    }
    return false;
  };
  
  function handleLeave() {
    if (!remember) {
      setDrop(null); 
    }
  };
  
  let Component = as || "div";
  return ( 
    <Component onDragEnter={(e) => dragItem && dropType === dragType && setDrop(dropId)} onDragOver={handleDragOver} onDrop={onDrop} style={{position: "relative", ...style}} {...props}>
      { children }
      { drop === dropId && <div style={{position: "absolute", inset: "0px"}} onDragLeave={handleLeave}></div> }
    </Component>
  );
};

// if we need multiple dropzones
Drag.DropZones = function({ dropType, prevId, nextId, split = "y", remember, children, ...props }) {
  const { dragType, isDragging } = React.useContext(DragContext);
  return (
    <div style={{position: "relative"}} {...props}>
      { children }
      { dragType === dropType && isDragging &&
        <div style={{position: "absolute", inset: "0px", display: "flex", flexDirection: split === "x" ? "row" : "column" }}>
          <Drag.DropZone dropId={prevId} style={{ width: "100%", height: "100%" }} dropType={dropType} remember={remember} />
          <Drag.DropZone dropId={nextId} style={{ width: "100%", height: "100%" }} dropType={dropType} remember={remember} />
        </div>
      }
    </div>
  );
};

// indicates where the drop will go when dragging over a dropzone
Drag.DropGuide = function({ as, dropId, dropType, ...props }) {
    const { drop, dragType } = React.useContext(DragContext);
    let Component = as || "div";
    return dragType === dropType && drop === dropId ? <Component {...props} /> : null;
};

// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

function Card({ title, description = "Drag and drop me!", dragItem }) {
  return (
    <div className={`rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2${ dragItem ? " rotate-6" : ""}`}>
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

function List({ name, dragItem, children }) {
  return (
    <div className={`rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow${ dragItem ? " rotate-6" : ""}`}>
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

// app component
function App() {
  const [data, setData] = React.useState(dummyData);
  
  // handle a dropped item
  function handleDrop({ dragItem, dragType, drop }) {
    if (dragType === "card") {
      // get the drop position as numbers
      let [newListPosition, newCardPosition] = drop.split("-").map((string) => parseInt(string));
      // create a copy for the new data
      let newData = structuredClone(data); // deep clone
      // find the current positions
      let oldCardPosition;
      let oldListPosition = data.findIndex((list) => {
        oldCardPosition = list.cards.findIndex((card) => card.id === dragItem);
        return oldCardPosition >= 0;
      });
      // get the card
      let card = data[oldListPosition].cards[oldCardPosition];
      // if same array and current position before drop reduce drop position by one
      if (newListPosition === oldListPosition && oldCardPosition < newCardPosition) {
        newCardPosition--; // reduce by one
      }
      // remove the card from the old position
      newData[oldListPosition].cards.splice(oldCardPosition, 1);
      // put it in the new position
      newData[newListPosition].cards.splice(newCardPosition, 0, card);
      // update the state
      setData(newData);
    }
  };
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <Drag handleDrop={handleDrop}>
        {({ activeItem, activeType, isDragging }) => (
          <Drag.DropZone className="flex -mx-2 overflow-x-scroll h-full">
            {data.map((list, listPosition) => {
              return (
                <React.Fragment key={list.id}>
                  <Drag.DropZone dropId={listPosition} dropType="list" remember={true}>
                    <Drag.DropGuide dropId={listPosition} dropType="list" className="rounded-xl bg-gray-200 h-96 mx-2 my-5 w-80 shrink-0 grow-0" />
                  </Drag.DropZone>
                  <Drag.DropZones className="flex flex-col h-full" prevId={listPosition} nextId={listPosition+1} dropType="list" split="x" remember={true}>
                    <Drag.DragItem dragId={list.id} className={`cursor-pointer ${activeItem === list.id && activeType === "list" && isDragging ? "hidden" : "translate-x-0"}`} dragType="list">
                      <List name={list.name} dragItem={activeItem === list.id && activeType === "list"}>
                        {data[listPosition].cards.map((card, cardPosition) => {
                          return (
                            <Drag.DropZones key={card.id} prevId={`${listPosition}-${cardPosition}`} nextId={`${listPosition}-${cardPosition+1}`} dropType="card" remember={true}>
                              <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                              <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
                                <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
                              </Drag.DragItem>
                            </Drag.DropZones>
                          );
                        })}
                        <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} dropType="card" remember={true}>
                          <Drag.DropGuide dropId={`${listPosition}-${data[listPosition].cards.length}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                        </Drag.DropZone>
                      </List>
                    </Drag.DragItem>
                    <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} className="grow" dropType="card" remember={true} />
                  </Drag.DropZones>
                </React.Fragment>
              );
            })}
            <Drag.DropZone dropId={data.length} dropType="list" remember={true}>
              <Drag.DropGuide dropId={data.length} dropType="list" className="rounded-xl bg-gray-200 h-96 mx-2 my-5 w-80 shrink-0 grow-0" />
            </Drag.DropZone>
          </Drag.DropZone>
        )}
      </Drag>
    </div>
  );
};


// ========================================
// render the react app
ReactDOM.render(
 <App />,
 document.getElementById('app')
);

Super - nearly finished!

Handle the list drop

Now we have the drop guides and drop zones in place for lists, let's handle the drop and get our lists moving.

Lets update handleDrop.

// handle a dropped item
function handleDrop({ dragItem, dragType, drop }) {
  if (dragType === "card") {
    // ...
  } else if (dragType === "list") {
    let newListPosition = drop;
    let oldListPosition = data.findIndex((list) => list.id === dragItem);
    // create a copy for the new data
    let newData = structuredClone(data); // deep clone
    // get the list
    let list = data[oldListPosition];
    // if current position before drop reduce drop position by one
    if (oldListPosition < newListPosition) {
      newListPosition--; // reduce by one
    }
    // remove list from the old position
    newData.splice(oldListPosition, 1);
    // put it in the new position
    newData.splice(newListPosition, 0, list);
    // update the state
    setData(newData);
  }
};

I've commented this out, it's very similar to how we moved cards - but for lists.

Now the lists move too!

<div id="app"></div>
// context for the drag
const DragContext = React.createContext();

// drag context component
function Drag({ draggable = true, handleDrop, children }) {
  const [dragType, setDragType] = React.useState(null); // if multiple types of drag item
  const [dragItem, setDragItem] = React.useState(null); // the item being dragged
  const [isDragging, setIsDragging] = React.useState(null);
  const [drop, setDrop] = React.useState(null); // the active dropzone
  
  React.useEffect(() => {
    if (dragItem) {
      document.body.style.cursor = "grabbing";
    } else {
      document.body.style.cursor = "default";
    }
  }, [dragItem]);
  
  const dragStart = function(e, dragId, dragType) {
    e.stopPropagation();
    e.dataTransfer.effectAllowed = 'move';
    setDragItem(dragId);
    setDragType(dragType);
  };
  
  const drag = function(e, dragId, dragType) {
    e.stopPropagation();
    setIsDragging(true);
  };
  
  const dragEnd = function(e) {
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  const onDrop = function(e) {
    e.preventDefault();
    handleDrop({ dragItem, dragType, drop });
    setDragItem(null);
    setDragType(null);
    setIsDragging(false);
    setDrop(null);
  };
  
  return (
    <DragContext.Provider value={{ draggable, dragItem, dragType, isDragging, dragStart, drag, dragEnd, drop, setDrop, onDrop }}>
      { typeof children === "function" ? children({ activeItem: dragItem, activeType: dragType, isDragging }) : children }
    </DragContext.Provider>
  );
};

// a draggable item
Drag.DragItem = function({ as, dragId, dragType, ...props }) {
  const { draggable, dragStart, drag, dragEnd, dragItem } = React.useContext(DragContext);
  
  let Component = as || "div";
  return <Component onDragStart={(e) => dragStart(e, dragId, dragType)} onDrag={drag} draggable={draggable} onDragEnd={dragEnd} {...props} />;
};

// listens for drags over drop zones
Drag.DropZone = function({ as, dropId, dropType, remember, children, style, ...props }) {
  const { dragItem, dragType, setDrop, drop, onDrop } = React.useContext(DragContext);
  
  function handleDragOver(e) {
    if (e.preventDefault) {
      e.preventDefault();
    }
    return false;
  };
  
  function handleLeave() {
    if (!remember) {
      setDrop(null); 
    }
  };
  
  let Component = as || "div";
  return ( 
    <Component onDragEnter={(e) => dragItem && dropType === dragType && setDrop(dropId)} onDragOver={handleDragOver} onDrop={onDrop} style={{position: "relative", ...style}} {...props}>
      { children }
      { drop === dropId && <div style={{position: "absolute", inset: "0px"}} onDragLeave={handleLeave}></div> }
    </Component>
  );
};

// if we need multiple dropzones
Drag.DropZones = function({ dropType, prevId, nextId, split = "y", remember, children, ...props }) {
  const { dragType, isDragging } = React.useContext(DragContext);
  return (
    <div style={{position: "relative"}} {...props}>
      { children }
      { dragType === dropType && isDragging &&
        <div style={{position: "absolute", inset: "0px", display: "flex", flexDirection: split === "x" ? "row" : "column" }}>
          <Drag.DropZone dropId={prevId} style={{ width: "100%", height: "100%" }} dropType={dropType} remember={remember} />
          <Drag.DropZone dropId={nextId} style={{ width: "100%", height: "100%" }} dropType={dropType} remember={remember} />
        </div>
      }
    </div>
  );
};

// indicates where the drop will go when dragging over a dropzone
Drag.DropGuide = function({ as, dropId, dropType, ...props }) {
    const { drop, dragType } = React.useContext(DragContext);
    let Component = as || "div";
    return dragType === dropType && drop === dropId ? <Component {...props} /> : null;
};

// the dummy Trello-style content
const dummyData = [
  { id: 1, name: "List 1", cards: [ 
    { id: 1, title: "Card 1" },
    { id: 2, title: "Card 2" },
    { id: 3, title: "Card 3" },
    { id: 4, title: "Card 4" },
    { id: 5, title: "Card 5" },
  ] },
  { id: 2, name: "List 2", cards: [ 
    { id: 6, title: "Card 6" },
    { id: 7, title: "Card 7" },
    { id: 8, title: "Card 8" },
  ] },
];

function Card({ title, description = "Drag and drop me!", dragItem }) {
  return (
    <div className={`rounded-lg bg-white border border-gray-300 shadow-sm p-5 m-2${ dragItem ? " rotate-6" : ""}`}>
      <h3 className="font-bold text-lg my-1">{ title }</h3>
      <p>{ description }</p>
    </div>
  );
};

function List({ name, dragItem, children }) {
  return (
    <div className={`rounded-xl bg-gray-100 p-2 mx-2 my-5 w-80 shrink-0 grow-0 shadow${ dragItem ? " rotate-6" : ""}`}>
      <div className="px-6 py-1">
        <h2 className="font-bold text-xl my-1">{ name }</h2>
      </div>
      { children }
    </div>
  );
};

// app component
function App() {
  const [data, setData] = React.useState(dummyData);
  
  // handle a dropped item
  function handleDrop({ dragItem, dragType, drop }) {
    if (dragType === "card") {
      // get the drop position as numbers
      let [newListPosition, newCardPosition] = drop.split("-").map((string) => parseInt(string));
      // create a copy for the new data
      let newData = structuredClone(data); // deep clone
      // find the current positions
      let oldCardPosition;
      let oldListPosition = data.findIndex((list) => {
        oldCardPosition = list.cards.findIndex((card) => card.id === dragItem);
        return oldCardPosition >= 0;
      });
      // get the card
      let card = data[oldListPosition].cards[oldCardPosition];
      // if same array and current position before drop reduce drop position by one
      if (newListPosition === oldListPosition && oldCardPosition < newCardPosition) {
        newCardPosition--; // reduce by one
      }
      // remove the card from the old position
      newData[oldListPosition].cards.splice(oldCardPosition, 1);
      // put it in the new position
      newData[newListPosition].cards.splice(newCardPosition, 0, card);
      // update the state
      setData(newData);
    } else if (dragType === "list") {
      let newListPosition = drop;
      let oldListPosition = data.findIndex((list) => list.id === dragItem);
      // create a copy for the new data
      let newData = structuredClone(data); // deep clone
      // get the list
      let list = data[oldListPosition];
      // if current position before drop reduce drop position by one
      if (oldListPosition < newListPosition) {
        newListPosition--; // reduce by one
      }
      // remove list from the old position
      newData.splice(oldListPosition, 1);
      // put it in the new position
      newData.splice(newListPosition, 0, list);
      // update the state
      setData(newData);
    }
  };
  
  return (
    <div className="p-10 flex flex-col h-screen">
      <h1 className="font-semibold text-3xl py-2">Trello-Style Drag & Drop</h1>
      <p>Let's drag some cards around!</p>
      <Drag handleDrop={handleDrop}>
        {({ activeItem, activeType, isDragging }) => (
          <Drag.DropZone className="flex -mx-2 overflow-x-scroll h-full">
            {data.map((list, listPosition) => {
              return (
                <React.Fragment key={list.id}>
                  <Drag.DropZone dropId={listPosition} dropType="list" remember={true}>
                    <Drag.DropGuide dropId={listPosition} dropType="list" className="rounded-xl bg-gray-200 h-96 mx-2 my-5 w-80 shrink-0 grow-0" />
                  </Drag.DropZone>
                  <Drag.DropZones className="flex flex-col h-full" prevId={listPosition} nextId={listPosition+1} dropType="list" split="x" remember={true}>
                    <Drag.DragItem dragId={list.id} className={`cursor-pointer ${activeItem === list.id && activeType === "list" && isDragging ? "hidden" : "translate-x-0"}`} dragType="list">
                      <List name={list.name} dragItem={activeItem === list.id && activeType === "list"}>
                        {data[listPosition].cards.map((card, cardPosition) => {
                          return (
                            <Drag.DropZones key={card.id} prevId={`${listPosition}-${cardPosition}`} nextId={`${listPosition}-${cardPosition+1}`} dropType="card" remember={true}>
                              <Drag.DropGuide dropId={`${listPosition}-${cardPosition}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                              <Drag.DragItem dragId={card.id} className={`cursor-pointer ${activeItem === card.id && activeType === "card" && isDragging ? "hidden" : "translate-x-0"}`} dragType="card">
                                <Card title={card.title} dragItem={activeItem === card.id && activeType === "card"} />
                              </Drag.DragItem>
                            </Drag.DropZones>
                          );
                        })}
                        <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} dropType="card" remember={true}>
                          <Drag.DropGuide dropId={`${listPosition}-${data[listPosition].cards.length}`} className="rounded-lg bg-gray-200 h-24 m-2" dropType="card" />
                        </Drag.DropZone>
                      </List>
                    </Drag.DragItem>
                    <Drag.DropZone dropId={`${listPosition}-${data[listPosition].cards.length}`} className="grow" dropType="card" remember={true} />
                  </Drag.DropZones>
                </React.Fragment>
              );
            })}
            <Drag.DropZone dropId={data.length} dropType="list" remember={true}>
              <Drag.DropGuide dropId={data.length} dropType="list" className="rounded-xl bg-gray-200 h-96 mx-2 my-5 w-80 shrink-0 grow-0" />
            </Drag.DropZone>
          </Drag.DropZone>
        )}
      </Drag>
    </div>
  );
};


// ========================================
// render the react app
ReactDOM.render(
 <App />,
 document.getElementById('app')
);

👏 And that's it for our Trello-like drag-and-drop board - I hope you enjoyed building it with me!