Building applications with drag-and-drop functionality can be overly complicated using traditional JavaScript. React DnD is a set of utilities that simplifies transferring data between different parts of your application, allowing you to easily create high-performance interfaces with drag-and-drop functionality.
React DnD is a perfect fit for apps like Trello, ProofHub, and ClickUp, which provide a UI for organizing your projects into different boards with drag and drop. In this tutorial, we’ll explore React DnD by building our own Trello clone. The code used in this tutorial is available on GitHub. Let’s get started!
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
React DnD includes a slew of great features that will save you development time and boost your application’s efficiency. Let’s review some of React DnD’s major perks.
In a technique similar to React Router and Flummox, React DnD covers your components, injecting props into them rather than offering prebuilt widgets. React DnD is built on React’s declarative rendering philosophy and therefore doesn’t modify the DOM. React DnD is an excellent complement to Redux and other unidirectional data flow architectures.
React DnD uses HTML5 drag and drop by default, but alternately, you can use the backend of your choice, creating your own custom events. Although drag and drop in HTML5 has a somewhat complicated API with several browser issues, React DnD handles these automatically, allowing you more time to focus on your project instead of troubleshooting.
Now that we understand the fundamentals behind React DnD, let’s build our Trello clone! First, create a new React project using the npx create-react-app command. For this tutorial, we’ll name our project trello-clone:
npx create-react-app trello-clone
Wait for the installation to finish, then change directories to the newly created folder. Next, we need to install React DnD and HTML drag and drop with the command below:
npm install react-dnd react-dnd-html5-backend react-modal
Now, let’s create our application’s components. In the src directory, create a component folder.
Column componentFirst, we’ll create a Column component that will serve as a wrapper for the other components we’ll create later in this tutorial. In Column, we’ll display all the content for our cards. Create a Column.jsx file and add the JSX code snippet below:
import react from "react";
const Column = ({isOver,children})=>{
const className = isOver ? "Highlight-region" : ""
return (
<div className={`col${className}`}></div>,
{children}
);
};
export default Column;
The code above creates a column where we’ll display all our cards. We’ll also need to apply a different style to our card when an item is dragged to it. We’ll use a ternary operator and a className variable, which will change depending on the state of the card.
CardItem componentLet’s create another component called ItemCard to handle our actual card items. We’ll also create a CardItem.jsx file inside our src/component folder.
First, we’ll import the following:
useEffect Hook: performs side effects in your componentsuseState Hook: manages our application’s stateuseRef Hook: persists objects throughout our componentFragment: enables us to group a list of child nodesWindow: refers to the Window component, which we’ll create shortlyTEMS_TYPE: specifies the type of item in our cardRun the command below to install the items listed above:
import react, { userEffect, Fragment, useRef, useState } from "react";
import { useDrag, useDrop } from 'react-dnd';
import Window from './Window';
import ITEM_TYPES from '../data/types'
Note that we haven’t created the data folder or its files yet. We’ll do that in a later section.
Next, let’s create our CardItem component, which will be responsible for moving items within the cards. Then, we’ll create a useRef Hook and destructure the drop object from the React useDrop Hook provided by React DnD.
We only need the drop object, so we’ll pass a , as the parameter of our object. We accept our item types, then pass our item and monitor to the hover parameter:
const cardItem = ({ item, index, moveItem, status }) => {
const ref = useRef(null)
const [, drop ] = useDrop({
accept: ITEM_TYPES,
hover({ item, monitor }) {
......
We’ll also check whether the item that we dragged was dropped in another card. If the item was not moved to another card, we don’t have to do anything to return the item:
if (!ref.current) {
return;
}
.....
However, if our item was dragged to another card, we need to move the item to that card:
const dragIndex = item.index;
const hoverIndex = item.hover;
if (dragIndex === hoverIndex) {
return;
}
const hovererdRect = ref.current.getBoundClientRect();
const hoverMiddleY = hovererdRect.bottom - hovererdRect.top / 2;
const mousePostion = monitor.getClientOffest();
const hoverClientY = mousePostion.y - hovererdRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
moveItem(dragIndex, hoverIndex);
item.index = hoverIndex;
......
In the code above, we created a dragIndex variable to check the index of the selected item. We created the hoverIndex variable to indicate the index of the item that is being moved. When the itemIndex is the same as the hoverItem, it means the item was not moved, in which case we’ll do nothing.
Then, we find hoverdRect, the hovered item rectangle position, hoverMiddleY, the hovered item middle y-axis position, mousePostion, the hovered item mouse position, and lastly, overClientY, the hovered item client y-axis position.
We performed a few checks to ensure that the selected item is properly moved to the intended card. Then, we moved the card and updated the index of the moved item.
Next, we need to destructure isDragging and drag from the useDrag Hook, which will require an item. In this case, we’ll use our item types, our actual items, and the item index. isDragging also take a collect callback function, which gives us a lot of props data supplied by React DnD.
Among the data provided by the collect callback is monitor, which refers to a copy of the screen that lets us know if we are dragging the screen:
const [{ isDragging }, drag] = useDrag({
item: { type: ITEM_TYPES, ...item, index },
collect: monitor => ({
isDragging: monitor.isDragging()
})
});
.......
We’ll create a state to handle the opening and closing of our card window, with onOpen and onClose handler functions to change the state of the card window being dragged. Then, we wrap the drag Hook with the drop Hook and pass in our ref Hook, helping us identify and locate the item that we’re working with:
const [show, setShow] = useState(false); const onOpen = () => setShow(true); const onClose = () => setShow(false) drag(drop(ref)) .......
Finally, we’ll use the <Fragment> component to return our grouped JSX element, displaying the card item data. When the object is in dragging mode, we set different opacity values and background colors from when it is dropped on a card. Then, we have our Window component rendered with the show value, item, and onClose function handler passed to it:
return (
<Fragment >
<div
ref={ref}
style={{ opacity: isDragging ? 0 : 1 }}
className={'item'}
onClick={onOpen}
>
<div className={'color-bar'} style={{ backgroundColor: status.color }}
></div>
<p className={'item-title'}>{item.content}</p>
</div>
<Window
item = {item}
onClose = {onclose}
show = {show}
/>
</Fragment>
)
}
export default cardItem
Window componentNow that we’re done with our CardItem component, let’s quickly create our Window component. In our src/component folder, create a Window.jsx file and add the following code snippet to it:
import React from "react";
import Modal from "react-modal";
Modal.setAppElement("#app");
In the code above, we import React and Modal from react-modal. We set the Modal to display on our application by using Modal.setAppElement and passing our app root element ID.
Next, we create our Window functional component, which gives us three props, show, onClose handler, and item. The show prop tells us whether to show the window or not. onClose controls what happens when the Window is closed, and Item props is the actual item in the Window:
>const Window = ({ show, onClose, item }) => {
.......
Then, we create our Modal and pass in the required props, isOpen, onRequestClose, and overlayClassName. We show the details of the items on the Modal and add a button to handle the Modal closing:
return (
<Modal
isOpen={show}
onRequestClose={onClose}
className={"modal"}
overlayClassName={"overlay"}
>
<div className={"close-btn-ctn"}>
<h1 style={{ flex: "1 90%" }}>{item.title}</h1>
<button className={"close-btn"} onClick={onclose}>
X
</button>
</div>
<div>
<h2>Description</h2>
<p>{item.content}</p>
<h2>Status</h2>
<p>
{item.icon}{" "}
{`${item.status.charAt(0).toUpperCase()}${item.status.slice(1)}`}
</p>
</div>
</Modal>
);
};
export default Window;
DropContainer componentWe’re done with our Window component, but we need a parent container that will enclose our Column and CardItem components as its child components, enabling us to have an indication of our card movements. First, we need to create a DropContainer.jsx file in our src/component folder and add the code snippet below to it:
import React from "react";
import { useDrop } from "react-dnd";
import ITEM_TYPES from "../data/types";
import { statuses } from "../data";
The code above will import the items types, which are statuses we created in our data folder to serve as our database, storing dummy data for our application.
Next, we create our DropContainer functional component, which also provides us with three props, onDrop, children, and status. We’ll need to destructure isOver and drop from the useDrop Hook, which takes accept, canDrop, Drop, and collect callbacks.
Then, we pass in our ITEM_TYPES to the accept callback and item and monitor to the canDrop callback. To know whether we can actually drop the item on a given card with the canDrop callback, we’ll get our item index and status.
Now, we can return an array of our items and their indexes, enabling the movement of our items either forward or backward on our cards:
const DropContainer = ({ onDrop, children, status }) => {
const [{ isOver }, drop] = useDrop({
accept: ITEM_TYPES,
canDrop: (item, monitor) => {
const itemIndex = statuses.findeIndex((si) => si.status === item.status);
const statusIndex = statuses.findIndex((si) => si.index === status);
return [itemIndex + 1, itemIndex - 1, itemIndex].includes(statusIndex);
},
........
On our onDrop callback, we’ll take an item and the monitor, which are the two parameters that we’ll need to drop an item. Then, we call the onDrop, passing the item, monitor, and the status of the item we are dropping.
We’ll use the collect callback to set the isOver props, which know if the item has been dropped in a card:
drop: (item, monitor) => {
onDrop(item, monitor, status);
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
});
........
Finally, we’ll return our HTML, referencing the drop Hook. We will use the React.cloneElement method to pass in a bunch of children elements to our parent component. Then, we export our DropContainer component:
return (
<div ref={drop} className={"drop-wrapper"}>
{React.cloneElement(children, { isOver })}
</div>
);
};
export default DropContainer;
Home componentNow let’s go ahead to create our homepage component. First, create a Home.jsx file in our src/component folder and add the code snippets below to it:
import React, { useState } from "react";
import CardItem from "./CardItems";
import DropContainer from "./DropConatiner";
import Column from "./Column";
import { data, statuses } from "../data";
...........
The code above imports our CardItem, DropContainer, and the Column component into our Home.jsx component. The code also imports data and statuses from our data folder.
Next, we create a state in our Home functional component to get an array of all the items in our data. We move the indexes of our items and change their status with the onDrop handler:
const Home = (props) => {
const [items, setItems] = useState(data);
const onDrop = (item, monitor, status) => {
setItems(prevState => {
const newItems = prevState
.filter(i => i.id !== item.id)
.concat({ ...item, status);
return [...newItems];
});
.........
We can control what happens when the item is moving by creating a moveItem handler, which takes two arguments, dragIndexand hoverIndex.
dragIndex is for the item being dragged, while hoverIndex is for items on hover. We filter the items’ array to get the items whose index is currently on the dragIndex object and insert the item into the new card:
const moveItem = (dragIndex, hoverIndex) => {
const item = items[dragIndex];
setItems(prevState => {
const newItems = prevState.filter((i, idx) => idx !== dragIndex);
newItems.splice(hoverIndex, 0, item);
return [...newItems];
});
};
...........
Next, we’ll loop through all our statuses using the map function, and return our columns with their status names. We’ll pass in our DropContainer, which accepts the OnDrop Hook and status. Now, we’ll display our columns, which will display only the items with the status in each of the columns. Then, we export our Home component:
return (
<div className={"row"}>
{statuses.map(s => {
return (
<div key={s.status} className={"col-wrapper"}>
<h4 className={"col-header"}>{s.status.toUpperCase}</h4>
<DropWrapper onDrop={onDrop} status={s.status}>
<Col>
{items
.filter(i => i.status === s.status)
.map((i, idx) => (
<Item
key={i.id}
item={i}
index={idx}
moveItem={moveItem}
status={s}
/>
))}
</Col>
</DropWrapper>
</div>
);
})}
</div>
);
Now, update the /src/App.jsx file with the following JSX code snippet:
........
import Homepage from "./component/Homepage";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
const App = () => {
return (
<DndProvider backend={HTML5Backend}>
<div className={"row"}>
<p className={"page-header"}>Trello Clone Dashboard</p>
</div>
<Homepage />
</DndProvider>
);
};
........
The code above wraps our entire application with the React DndProvider, passing the HTML5Backend, which we import from the React dnd HTML5 backend.
Next, make a data folder in our src directory folder. In the data folder, create a data.js and a types.js file. Add the code snippet below to the data.js file:
const data = [
{
id: 1,
status: "open",
title: "Available Topic",
content: "Buiding a REST API with Django",
},
{
id: 2,
status: "open",
title: "Sponsored Post",
content: "How to create a React Chat Application",
},
{
id: 3,
status: "open",
title: "Editing",
content: "Building a Trello clone with React DnD",
},
{
id: 4,
status: "open",
title: "Invoicing",
content: "Inro To Web3 ",
},
];
const statuses = [
{
index: 1,
status: "open",
color: "#EB5A46",
},
{
index: 2,
status: "in progress",
color: "#00C2E0",
},
{
index: 3,
status: "in review",
color: "#C377E0",
},
{
index: 4,
status: "done",
color: "#3981DE",
},
];
export { data, statuses };
Then, add the code snippet below to the types.js file:
const ITEM_TYPES = 'ITEM' export default ITEM_TYPES
Now, let’s style our components. Create a style.css file in your src directory and add the code snippet below to it:
:root {
--primary-color: rgb(62, 100, 255);
--complete-color: #27aa80;
--text-color: #172b4d;
--disabled-color: #fad6d6;
--background-color: #f5eaea;
}
html {
background: rgb(0,73,191);
background: linear-gradient(90deg, rgba(0,73,191,1) 0%, rgba(190,190,255,1) 46%, rgba(0,212,255,1) 100%);
}
body {
color: var(--text-color);
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Droid Sans,Helvetica Neue,sans-serif;
margin: 0;
background-image: url("https://trello-backgrounds.s3.amazonaws.com/SharedBackground/2400x1600/ef3a46a026c718b8329c0c34b0a57108/photo-1550592704-6c76defa9985.jpg");
background-size: cover;
}
a {
color: unset;
text-decoration: unset;
cursor: pointer;
}
p {
margin: 10px 0;
overflow-wrap: break-word;
text-align: left;
}
label {
font-size: 16px;
display: block;
}
button, input {
padding: 4px;
border: 1px solid var(--disabled-color);
}
button {
outline: none;
background: transparent;
border-radius: 5px;
color: var(--primary-color);
transition: all ease 0.8s;
cursor: pointer;
}
button.active {
color: var(--primary-color);
}
button.active:after {
content: "";
display: block;
margin: 0 auto;
width: 50%;
padding-top: 4px;
border-bottom: 1px solid var(--primary-color);
}
input:focus {
outline: none;
}
select {
outline: none;
height: 40px;
}
.row {
display: flex;
flex-direction: row;
justify-content: center;
}
.item {
font-size: 15px;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
z-index: 1;
background-color: white;
}
.item:hover {
cursor: pointer;
}
.item-title {
font-weight: 600;
font-size: 16px;
}
.item-status {
text-align: right;
}
.color-bar {
width: 40px;
height: 10px;
border-radius: 5px;
}
.drop-wrapper {
flex: 1 25%;
width: 100%;
height: 100%;
}
.col-wrapper {
display: flex;
flex-direction: column;
margin: 20px;
padding: 20px;
background-color: var(--background-color);
border-radius: 5px;
}
.col-header {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
margin-top: 0;
}
.col {
min-height: 300px;
max-width: 300px;
width: 300px;
}
.highlight-region {
background-color: yellow;
}
.page-header {
background-color: #10131470;
padding: 20px;
color: white;
font-size: 20px;
flex: 1 100%;
margin-top: 0;
text-align: left;
font-weight: bolder;
}
.modal {
background-color: #F4F5F7;
border-radius: 2px;
margin: 48px 0 80px;
min-height: 450px;
width: 800px;
outline: none;
padding: 20px;
}
.overlay {
display: flex;
justify-content: center;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);;
}
.close-btn-ctn {
display: flex;
}
.close-btn {
height: 40px;
width: 35px;
font-size: 20px;
color: #031D2C;
border: none;
border-radius: 25px;
}
.close-btn:hover {
background-color: #DCDCDC;
}
In this tutorial, we learned how we can easily implement drag-and-drop functionality in our applications using React DnD. To put our knowledge into practice, we built a clone of Trello, a UI tool for organizing projects into columns using drag and drop.
You can follow the steps outlined in this tutorial to build all kinds of applications that require drag and drop. I hope you enjoyed this tutorial!
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now