if you want to create draggable/droppable elements on your website one of the options available is Dnd-kit which is a modern drag-and-drop toolkit for React. Using it is fairly very easy, unless you end up on youtube where for some reason every one wants to make kanban boards with it and suddenly instead of learning how to use Dnd-kit now you are also dealing with the added complexity of a kanban board. So, I am just going to do a very simple and straightforward setup for Dnd-kit just going over how to use it and what we are doing.
Don't want to read just want the code? Here is the 🔗 git link
Let's Breakdown our steps
- 🛠️ Create a React Project with Vite.
- 🪄 Adding Dnd-context
- 👀 Do some very Basic page setup like giving the page full height/width etc.
- 🖱️ Making a Draggable Item.
- 📦 Making a Droppable Item.
- 🔨 Import both the elements.
- 🧠 Telling Dnd what to do on starting and ending the drag.
- 🔜 Playing with DragOverlay to have smooth transitions (coming soon, stay tuned).
1. 🛠️ Creating project with vite
Use npm create vite@latest . -- --template react
if you are already in the folder where you wish to create or try or
npm create vite@latest my-proj -- --template react
which will create a directory with name my-proj
.
In the last command the .
denoted that we want to create this react project in the directory we are in.
run npm install
to install the required node packages.
Remove everything from the App.jsx
it should look something like this.
import "./App.css";
function App() {
return <>Hello</>;
}
export default App;
This will give us a clean slate to work with.
I have kept the default styles from the template because I am lazy.
2. 🪄 Installing Dnd-kit and wrapping in DndContext
We can use npm install @dnd-kit/core
to install Dnd-kit to our project.
once installed we can wrap our webapp in DndContext which is like the central controller for Dnd-kit, and without this drag and drop will not work Also add some containers to keep our items in the future. Our App.jsx
will look something like this.
import { DndContext } from "@dnd-kit/core";
import "./App.css";
function App() {
return (
<>
<DndContext>
<div className="webapp">
<div className="droppable-container"></div>
<div className="draggable-container"></div>
</div>
</DndContext>
</>
);
}
export default App;
3. 👀 Basic Page Setup 👀
I just created 2 containers one will hold all the draggable items initially and all elements will be dropped in to the droppable container which we will change soon. This is how the App.css
looks like after the changes.
body {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
}
#root {
height: 100%;
width: 100%;
}
.webapp {
height: 100%;
width: 100%;
display: flex;
gap: 5%;
justify-content: space-evenly;
align-items: center;
}
.droppable-container {
outline: green solid;
width: 40%;
height: 80%;
display: grid;
grid-template-columns: auto auto auto;
align-items: center;
justify-content: space-evenly;
}
.draggable-container {
outline: solid red;
width: 40%;
height: 80%;
display: grid;
grid-template-columns: auto auto auto;
align-items: center;
justify-content: space-evenly;
}
.drag-item {
background-color: antiquewhite;
height: 100px;
width: 100px;
cursor: pointer;
color: black;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
4. 🖱️ Let's Create a simple Draggable component
Create a components directory in src and a 'draggableItem' directory within this directory.
Then create a DraggableItem.jsx
file.
As mentioned most of the heavy lifting gets done by Dnd-kit so following the documentation the Draggable Item will look something like this.
import { useDraggable } from "@dnd-kit/core";
import { CSS } from "@dnd-kit/utilities";
const Draggable = (props) => {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: props.id,
});
const style = {
transform: CSS.Translate.toString(transform),
};
return (
<div
className="drag-item"
style={style}
ref={setNodeRef}
{...listeners}
{...attributes}
>
{props.children}
</div>
);
};
export default Draggable;
The props.id
is the most important as it defines what element becomes draggable if you plan on having just one then you can hard assign the id in the component but if you are creating multiple then it is better to receive it from the parent.
and using const { attributes, listeners, setNodeRef, transform } = useDraggable({ id: props.id, });
spreads all the needed stuff from Dnd-kit.
and this line const style = { transform: CSS.Translate.toString(transform), };
makes the element visually move on the screen.
and here we basically assign everything to our draggable container
return (
<div
className="drag-item"
style={style}
ref={setNodeRef}
{...listeners}
{...attributes}
>
{props.children}
</div>
);
props.children is basically everything that we will wrap under the Draggable
component in the main file App.jsx
5. 📦 Let's create a Droppable component
Once done the Droppable.jsx
should look something like this.
import { useDroppable } from "@dnd-kit/core";
const Droppable = (props) => {
const { isOver, setNodeRef } = useDroppable({
id: "drop-container",
});
return (
<div className="droppable-container" ref={setNodeRef}>
{props.children}
</div>
);
};
export default Droppable;
Here we only want a single droppable container so we can assign the id directly here. and the isOver
tells DndContext if an element is over this droppable container, and once we drop the item on this container we use props.children
to render all the dropped items in the droppable container.
6. 🔨 Let's import both the elements to App.jsx
I have also created an array of some items so we can have multiple draggable items and then we can map over them to create the draggable items we need unique id's for all so we will use the index to create these Draggable items.
import { DndContext } from "@dnd-kit/core";
import "./App.css";
import Droppable from "./components/droppableItem/Droppable";
import Draggable from "./components/draggableItem/Draggable";
import { useState } from "react";
function App() {
const [items, setItems] = useState(["Cat", "Dog", "Mansion", "Car", "Lake"]);
return (
<>
<DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
<div className="webapp">
<Droppable>
{droppedItems.map((id) => (
<Draggable key={id} id={id}>
{`${id}`}
</Draggable>
))}
</Droppable>
<div className="draggable-container">
{items.map((id) => (
<Draggable key={id} id={id}>
{`${id}`}
</Draggable>
))}
</div>
</div>
</DndContext>
</>
);
}
export default App;
We can also make some styling changes to the Draggable container so that all elements are spread out properly
.draggable-container {
outline: solid red;
width: 40%;
height: 80%;
display: grid;
grid-template-columns: auto auto auto;
align-items: center;
justify-content: space-evenly;
}
and also style the drag Items.
.drag-item {
background-color: antiquewhite;
height: 100px;
width: 100px;
cursor: pointer;
color: black;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
At this point you should be able to at least drag around your items.
But you won't be able to drop them anymore, that is because we still have not configured the Drop logic. let's get to it.
7. 🧠 Telling DND what to do on Drag start and End.
To do so we will create 2 functions onDragStart
and onDragEnd
and create some state variables const [droppedItems, setDroppedItems] = useState([])
This will hold the list of all the elements dropped in the container. const [activeId, setActiveId] = useState(null)
and this will keep track of what element we are dragging. and set them in the dndContext.
const [droppedItems, setDroppedItems] = useState([])
const [activeId, setActiveId] = useState(null)
<DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
first on dragStart we need to set the activeId
to the element being dragged.
const onDragStart = (event) =>{
setActiveId(event.active.id)
}
And when the drag ends
We need to check if it was dropped over our droppable container or not and if it was then check if that item already existed in there or not we do not want to duplicate the same element, then we need to remove the dragged element from the items
list to the droppedItems
list and remember to remove the activeId
.
Something like this,
const onDragEnd = (event) => {
const {active, over} = event
if (!over) return
if(over.id ==='drop-container'){
setItems(items => items.filter(item => item!= activeId))
setDroppedItems([activeId, ...droppedItems])
}
setActiveId(null)
}
After this you should be able to drop your elements to the droppable container, but you cannot remove them from it, but it is really easy to do so as well.
This time:
- We will check if the item was dropped over nothing (outside the container).
- Check if that element was inside the droppable container before the drag or not.
- if not, do nothing element was out stays out
- if yes, remove from the
droppedItems
list and add back to theitems
list.
And now your dragEnd
function should look something like this.
const onDragEnd = (event) => {
const {active, over} = event
if (!over){
if (droppedItems.includes(activeId)){
setDroppedItems((items) => items.filter((item) => item != activeId));
setItems([...items, activeId])
}
setActiveId(null)
return
}
if(over.id ==='drop-container' && !droppedItems.includes(activeId)){
setItems(items => items.filter(item => item!= activeId))
setDroppedItems([activeId, ...droppedItems])
}
setActiveId(null)
}
and you should be able to drag and drop stuff in the container and outside the container to remove it.
8.🧠 Playing with DragOverlay to have smooth transitions
We can also animate the drag and drop of the elements with the help of dragOverlay
which is also very easy. I will be creating a new article for the same soon, so stay tuned for that :)
Feel free to drop a comment in case something seems wrong or you are unable to replicate the same functionality, would love to help 😄.
In the end here is all the code and the 🔗 git link
App.jsx
import { DndContext } from "@dnd-kit/core";
import "./App.css";
import Droppable from "./components/droppableItem/Droppable";
import Draggable from "./components/draggableItem/Draggable";
import { useState } from "react";
function App() {
const [items, setItems] = useState(["Cat", "Dog", "Mansion", "Car", "Lake"]);
const [droppedItems, setDroppedItems] = useState([])
const [activeId, setActiveId] = useState(null)
const onDragStart = (event) =>{
setActiveId(event.active.id)
}
const onDragEnd = (event) => {
const {active, over} = event
if (!over){
if (droppedItems.includes(activeId)){
setDroppedItems((items) => items.filter((item) => item != activeId));
setItems([...items, activeId])
}
setActiveId(null)
return
}
if(over.id ==='drop-container' && !droppedItems.includes(activeId)){
setItems(items => items.filter(item => item!= activeId))
setDroppedItems([activeId, ...droppedItems])
}
setActiveId(null)
}
return (
<>
<DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
<div className="webapp">
<Droppable>
{droppedItems.map((id) => (
<Draggable key={id} id={id}>
{`${id}`}
</Draggable>
))}
</Droppable>
<div className="draggable-container">
{items.map((id) => (
<Draggable key={id} id={id}>
{`${id}`}
</Draggable>
))}
</div>
</div>
</DndContext>
</>
);
}
export default App;
Droppable.jsx
import { useDroppable } from "@dnd-kit/core";
const Droppable = (props) => {
const { isOver, setNodeRef } = useDroppable({
id: "drop-container",
});
return (
<div className="droppable-container" ref={setNodeRef}>
{props.children}
</div>
);
};
export default Droppable;
Draggable.jsx
import { useDraggable } from "@dnd-kit/core";
import { CSS } from "@dnd-kit/utilities";
const Draggable = (props) => {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: props.id,
});
const style = {
transform: CSS.Translate.toString(transform),
};
return (
<div
className="drag-item"
style={style}
ref={setNodeRef}
{...listeners}
{...attributes}
>
{props.children}
</div>
);
};
export default Draggable;
App.css
body {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
}
#root {
height: 100%;
width: 100%;
}
.webapp {
height: 100%;
width: 100%;
display: flex;
gap: 5%;
justify-content: space-evenly;
align-items: center;
}
.droppable-container {
outline: green solid;
width: 40%;
height: 80%;
display: grid;
grid-template-columns: auto auto auto;
align-items: center;
justify-content: space-evenly;
}
.draggable-container {
outline: solid red;
width: 40%;
height: 80%;
display: grid;
grid-template-columns: auto auto auto;
align-items: center;
justify-content: space-evenly;
}
.drag-item {
background-color: antiquewhite;
height: 100px;
width: 100px;
cursor: pointer;
color: black;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
Cover image taken from craiyon AI