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.
Create a Next.js Typescript project by running the code snippet below:
npx create-next-app drag-n-drop
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;
}
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;
};
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>
);
}
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
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>
);
}
- 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 theIssueCard
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>
);
}
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
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>
);
}
- 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.
- The
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 });
};
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]);
}
}
};
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>
);
}
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>
);
}
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)