DEV Community

Cover image for How to implement drag and drop in React using dnd-kit
David Asaolu
David Asaolu

Posted on

How to implement drag and drop in React using dnd-kit

As a developer, you will encounter various projects where you'll need to implement drag and drop functionality. Whether you're building kanban boards, e-commerce platforms with custom cart interactions or even games, drag and drop enhances the overall user experience of your application.

Despite its widespread use, implementing drag and drop can be challengingβ€”especially when building custom interfaces that need to handle complex interactions. That’s where dnd-kit, a powerful and developer-friendly open-source library, comes in.

In this tutorial, you'll learn how to implement drag and drop easily in React or Nextjs applications, enabling you track and respond to user interactions within your application.


What is dnd-kit?

Dnd-kit is a simple, lightweight, and modular library that enables you to implement drag and drop functionality in React applications. It provides two hooks: useDraggable and useDroppable, allowing you to integrate and respond to drag and drop events.

Dnd-kit also supports various use cases such as: lists, grids, multiple containers, nested contexts, variable sized items, virtualized lists, 2D Games, and many more.

Next, let's see dnd-kit in action.


Project Set up and Installation

To demonstrate how dnd-kit works, we'll build a simple task manager app. Its interface will help the customer support team efficiently track and resolve issues within the application.

PS: The complete application, including authentication, database interactions and notifications, will be available in the coming week.

App Overview

Create a Next.js Typescript project by running the code snippet below:

npx create-next-app drag-n-drop
Enter fullscreen mode Exit fullscreen mode

Add a types.d.ts file at the root of the Next.js project and copy the code snippet below into the file:

type ColumnStatus = {
    status: "new" | "open" | "closed";
};
type ColumnType = {
    title: string;
    id: ColumnStatus["status"];
    issues: IssueType[];
    bg_color: string;
};
type IssueType = {
    id: string;
    title: string;
    customer_name: string;
    customer_email?: string;
    content?: string;
    attachmentURL?: string;
    messages?: Message[];
    date: string;
    status: ColumnStatus["status"];
};
interface Message {
    id: string;
}
Enter fullscreen mode Exit fullscreen mode

The code snippet above defines the data structure for the variables used within the application.


Building the application interface

In this section, I'll walk you through building the application interface for the application.

Before we proceed, create a utils folder containing a lib.ts file and copy the following code snippet into the file:


//πŸ‘‡πŸ» default list of new issues
export const newIssuesList: IssueType[] = [
    {
        id: "1",
        customer_name: "David",
        title: "How can I access my account?",
        date: "25th December, 2025",
        status: "new",
    },
];

//πŸ‘‡πŸ» default list of open issues
export const openIssuesList: IssueType[] = [
    {
        id: "2",
        customer_name: "David",
        title: "My password is not working and I need it fixed ASAP",
        date: "20th July, 2023",
        status: "open",
    },
    {
        id: "3",
        customer_name: "David",
        title: "First Issues",
        date: "5th February, 2023",
        status: "open",
    },
    {
        id: "4",
        customer_name: "David",
        title: "First Issues",
        date: "2nd March, 2023",
        status: "open",
    },
    {
        id: "5",
        customer_name: "David",
        title:
            "What is wrong with your network? I can't access my profile settings account",
        date: "5th August, 2024",
        status: "open",
    },
];

//πŸ‘‡πŸ» default list of closed issues
export const closedIssuesList: IssueType[] = [
    {
        id: "6",
        customer_name: "David",
        title: "First Issues",
        date: "2nd March, 2023",
        status: "closed",
    },
    {
        id: "7",
        customer_name: "Jeremiah Chibuike",
        title:
            "What is wrong with your network? I can't access my profile settings account",
        date: "5th August, 2024",
        status: "closed",
    },
    {
        id: "8",
        customer_name: "David",
        title: "First Issues",
        date: "2nd March, 2023",
        status: "closed",
    },
    {
        id: "9",
        customer_name: "David",
        title:
            "What is wrong with your network? I can't access my profile settings account",
        date: "5th August, 2024",
        status: "closed",
    },
    {
        id: "10",
        customer_name: "David",
        title:
            "What is wrong with your network? I can't access my profile settings account",
        date: "5th August, 2024",
        status: "closed",
    },
];

//πŸ‘‡πŸ» Helper function to find and remove an issue from a list
export const findAndRemoveIssue = (
    issues: IssueType[],
    setIssues: React.Dispatch<React.SetStateAction<IssueType[]>>,
    currentIssueId: string
): IssueType | null => {
    const issueIndex = issues.findIndex((issue) => issue.id === currentIssueId);
    if (issueIndex === -1) return null; //πŸ‘ˆπŸΌ Not found

    const [removedIssue] = issues.splice(issueIndex, 1);
    setIssues([...issues]); //πŸ‘ˆπŸΌ Update state after removal
    return removedIssue;
};
Enter fullscreen mode Exit fullscreen mode

The code snippet above includes arrays of issues grouped by their status. The findAndRemoveIssue function accepts three arguments: an issues array, a setIssues function for updating the array, and the ID of the issue to be acted upon.

Update the app/page.tsx file to render the issues in their respective columns.

"use client";
import Link from "next/link";
import Column from "@/app/components/Column";
import { useState } from "react";
import { closedIssuesList, newIssuesList, openIssuesList } from "./utils/lib";

export default function App() {
    const [newIssues, setNewIssues] = useState<IssueType[]>(newIssuesList);
    const [openIssues, setOpenIssues] = useState<IssueType[]>(openIssuesList);
    const [closedIssues, setClosedIssues] =
        useState<IssueType[]>(closedIssuesList);

    return (
        <main>
            <nav className='w-full h-[10vh] flex items-center justify-between px-8 bg-blue-100 top-0 sticky z-10'>
                <Link href='/' className='font-bold text-2xl'>
                    Suportfix
                </Link>
                <Link
                    href='/login'
                    className='bg-blue-500 px-4 py-3 rounded-md text-blue-50'
                >
                    SUPPORT CENTER
                </Link>
            </nav>

            <div className='w-full min-h-[90vh] lg:p-8 p-6 flex flex-col lg:flex-row items-start justify-between lg:space-x-4'>
                <Column
                    bg_color='red'
                    id='new'
                    title={`New (${newIssues.length})`}
                    issues={newIssues}
                />

                <Column
                    bg_color='purple'
                    id='open'
                    title={`Open (${openIssues.length})`}
                    issues={openIssues}
                />

                <Column
                    bg_color='green'
                    id='closed'
                    title={`Closed (${closedIssues.length})`}
                    issues={closedIssues}
                />
            </div>
        </main>
    );
}
Enter fullscreen mode Exit fullscreen mode

Dashboard Page

Next, let's create the Column component. Add a components folder within the Next.js app folder as shown below:

cd app
mkdir components && cd components
touch Column.tsx
Enter fullscreen mode Exit fullscreen mode

Copy the following code snippet into the Column.tsx file to render the issues under their respective columns.

import { bgClasses, headingClasses } from "../utils/lib";
import IssueCard from "./IssueCard";

export default function Column({ title, id, bg_color, issues }: ColumnType) {
    return (
        <div
            className={`lg:w-1/3 w-full p-4 min-h-[50vh] rounded-md shadow-md lg:mb-0 mb-6 ${
                bgClasses[bg_color] || ""
            } `}
            key={id}
        >
            <header className='flex items-center justify-between'>
                <h2 className={`font-bold text-xl mb-4 ${headingClasses[bg_color]}`}>
                    {title}
                </h2>
                {issues?.length > 4 && (
                    <button className='text-gray-500 underline text-sm'>Show More</button>
                )}
            </header>

            <div className='flex flex-col w-full items-center space-y-4'>
                {issues?.map((item) => (
                    <IssueCard
                        item={item}
                        key={item.id}
                        bg_color={bg_color}
                        columnId={id}
                    />
                ))}
            </div>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode
  • From the code snippet above, the Column component accepts the following props:
    • bg_color represents the column colour,
    • id represents the column id,
    • title is the column title, and
    • issues is the array of issues under each column. They are rendered within the IssueCard component.

Next, add the IssueCard component to the components folder and copy the following code snippet into the file:

import { IoIosChatbubbles } from "react-icons/io";
import { borderClasses } from "@/app/utils/lib";

export default function IssueCard({
    item,
    bg_color,
    columnId,
}: {
    item: IssueType;
    bg_color: string;
    columnId: ColumnStatus["status"];
}) {
    return (
        <div
            className={`w-full min-h-[150px] cursor-grab rounded-md bg-white z-5 border-[2px] p-4 hover:shadow-lg ${
                borderClasses[bg_color] || ""
            }`}
        >
            <h2 className='font-bold mb-3 text-gray-700 opacity-80'>{item.title}</h2>

            <p className='text-sm opacity-50 mb-[5px]'>Date created: {item.date}</p>
            <p className='text-sm opacity-50 mb-[5px]'>
                Created by: {item.customer_name}
            </p>

            <section className='flex items-center justify-end space-x-4'>
                <button className='px-4 py-2 text-sm text-white bg-blue-400 hover:bg-blue-500 flex items-center rounded'>
                    Chat <IoIosChatbubbles className='ml-[3px]' />
                </button>
                {columnId !== "closed" ? (
                    <button className='px-4 py-2 text-sm text-white bg-red-400 rounded hover:bg-red-500'>
                        Close Ticket
                    </button>
                ) : (
                    <button className='px-4 py-2 text-sm text-white bg-green-400 rounded hover:bg-green-500'>
                        Reopen Ticket
                    </button>
                )}
            </section>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Congratulations! You've designed the application interface. In the upcoming sections you'll learn how to add the drag and drop functionality.


Dragging and Dropping elements easily in React

In the previous section, we created two components: Column and IssueCard. The IssueCard component represents each draggable issue within a column, while the Column component contains a list of issues.

Here, we'll use the dnd-kit package to make the issues draggable and droppable across the three columns.

Install the dnd-kit package by the running the following code snippet:

npm install @dnd-kit/core
Enter fullscreen mode Exit fullscreen mode

Update the app/page.tsx file as shown below:

import { DragEndEvent, DndContext } from "@dnd-kit/core";

export default function App() {
    //πŸ‘‡πŸ» listens for drag end events
    const handleDragEnd = (event: DragEndEvent) => {};

    return (
        <main>
            {/** -- other UI elements -- */}
            <div className='w-full min-h-[90vh] lg:p-8 p-6 flex flex-col lg:flex-row items-start justify-between lg:space-x-4'>
                <DndContext onDragEnd={handleDragEnd}>
                    <Column
                        bg_color='red'
                        id='new'
                        title={`New (${newIssues.length})`}
                        issues={newIssues}
                    />

                    <Column
                        bg_color='purple'
                        id='open'
                        title={`Open (${openIssues.length})`}
                        issues={openIssues}
                    />

                    <Column
                        bg_color='green'
                        id='closed'
                        title={`Closed (${closedIssues.length})`}
                        issues={closedIssues}
                    />
                </DndContext>
            </div>
        </main>
    );
}
Enter fullscreen mode Exit fullscreen mode
  • From the code snippet above,
    • The DndContext provider wraps the entire draggable and droppable components and enables you to access the various props required to track and respond to drag and drop events.
    • The DragEndEvent enables you to trigger events after a drag action.

Next, update the handleDragEnd function to move each issue to its new column when it is dragged from its column to another column.

const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;
        //πŸ‘‡πŸ» no reaction if it is not over a column
    if (!over) return;
    const issueId = active.id as string;
    const newStatus = over.id as ColumnStatus["status"];
        //logs the issue id and id of its new column
    console.log({ issueId, newStatus });
};
Enter fullscreen mode Exit fullscreen mode

The handleDragEnd function logs the issue id and the id of its new column when it is dragged over any of the column.

Add the following code snippet to the handleDragEnd function:

const handleDragEnd = (event: DragEndEvent) => {
    const { active, over } = event;
    if (!over) return;
    const issueId = active.id as string;
    const newStatus = over.id as ColumnStatus["status"];

    let movedIssue: IssueType | null = null;

    //πŸ‘‡πŸ» Find and remove the issue from its current state
    movedIssue =
        movedIssue ||
        findAndRemoveIssue(newIssues, setNewIssues, issueId) ||
        findAndRemoveIssue(openIssues, setOpenIssues, issueId) ||
        findAndRemoveIssue(closedIssues, setClosedIssues, issueId);

    //πŸ‘‡πŸ» If an issue was successfully removed, add it to the new column
    if (movedIssue) {
        movedIssue.status = newStatus; // πŸ‘ˆπŸΌ Update the status of the issue
        if (newStatus === "new") {
            setNewIssues((prev) => [...prev, movedIssue]);
        } else if (newStatus === "open") {
            setOpenIssues((prev) => [...prev, movedIssue]);
        } else if (newStatus === "closed") {
            setClosedIssues((prev) => [...prev, movedIssue]);
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

The handleDragEnd function moves the selected issue (item) from its current column to the new column when it is dragged over a new column.

Within the Column component, import the useDroppable hook and initialize it with the column ID. Then, assign the returned setNodeRef function to the parent <div> as a reference to enable the droppable functionality.

import { useDroppable } from "@dnd-kit/core";

export default function Column({ title, id, bg_color, issues }: ColumnType) {
    const { setNodeRef } = useDroppable({ id });

    return (
        <div
            className={`lg:w-1/3 w-full p-4 min-h-[50vh] rounded-md shadow-md lg:mb-0 mb-6 ${
                bgClasses[bg_color] || ""
            } `}
            key={id}
            ref={setNodeRef}
        >
            {/** -- UI elements -- */}
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

To make each issue draggable, use the useDraggable hook and destructure the required props: attributes, listeners, setNodeRef, and transform. Pass the issue's unique id as an argument to the hook.

Update the IssueCard component as follows:

import { useDraggable } from "@dnd-kit/core";
import { CSS } from "@dnd-kit/utilities";

export default function IssueCard({
    item,
    bg_color,
    columnId,
}: {
    item: IssueType;
    bg_color: string;
    columnId: ColumnStatus["status"];
}) {
    //πŸ‘‡πŸ» required props to enable each issue to be draggable
    const { attributes, listeners, setNodeRef, transform } = useDraggable({
        id: item.id,
    });

    //πŸ‘‡πŸ» required styles to track the position of each issue
    const style = { transform: CSS.Translate.toString(transform) };

    return (
        <div
            className={`w-full min-h-[150px] cursor-grab rounded-md bg-white z-5 border-[2px] p-4 hover:shadow-lg ${
                borderClasses[bg_color] || ""
            }`}
            style={style}
            ref={setNodeRef}
            {...listeners}
            {...attributes}
        >
            {/** -- UI elements -- */}
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

The style object enables dnd-kit to track the position of the issue on the screen.

Congratulations! You've completed this tutorial. You can check out the live version.


Conclusion

So far, you've learnt how to implement drag and drop functionality in React apps using the dnd-kit package. It is a lightweight and powerful solution for adding dynamic user interactions to your applications.

If you're looking for a more in-depth explanation, be sure to check out the video tutorial here:

The source code for this tutorial is available here:
https://github.com/dha-stix/drag-n-drop-tutorial-with-dnd-kit

Thank you for reading, and happy coding!

Top comments (0)