How to Drag and Drop in React Without Extra Libraries

How to Drag and Drop in React Without Extra Libraries

With the help of the HTML Drag and Drop API
Ferenc Almasi β€’ 2023 March 27 β€’ Read time 12 min read
Learn how you can build a drag-and-drop Kanban board in React with the help of the native HTML Drag and Drop API.
  • twitter
  • facebook
React Previous Tutorial

Drag and drop functionality is a common feature that creates a more intuitive user flow by letting users grab and drop objects to places where they want them to be. There are countless JavaScript libraries out there just for this purpose, including one for every possible framework; however, this can be done quite simply with the native Drag and Drop API.

drag and drop in React
The final project, a drag-and-drop Kanban board in React

At the end of this tutorial, you will learn how to create the above drag-and-drop functionality for a Kanban board in React using the native HTML Drag and Drop API.


Setting Up the Component

Let’s start by setting up the component. We are going to store the cards in a useState hook. In a real-world application, you would fetch this data from an API. For the sake of simplicity, however, we are going to mock the initial state. Create a new component and add the following:

Copied to clipboard! Playground
import { useState } from 'react'

const initialState = [
    { id: 1, name: 'Todo #1', state: 'todo' },
    { id: 2, name: 'Todo #2', state: 'todo' },
    { id: 3, name: 'Todo #3', state: 'todo' },
    { id: 4, name: 'IP #1', state: 'ip' },
    { id: 5, name: 'IP #2', state: 'ip' },
    { id: 6, name: 'IP #3', state: 'ip' },
    { id: 7, name: 'Done #1', state: 'done' },
    { id: 8, name: 'Done #2', state: 'done' },
    { id: 9, name: 'Done #3', state: 'done' }
]

const DragAndDrop = () => {
    const [cards, setCards] = useState(initialState)

    return (
        <main className="board">
            <div className="column column-todo">
                <h2>Todo</h2>
                {cards.filter(card => card.state === 'todo').map(todo => (
                    <article key={todo.id} className="card">
                        <h3>{todo.name}</h3>
                    </article>
                ))}
            </div>
            <div className="column column-ip">...</div>
            <div className="column column-done">...</div>
        </main>
    )
}

export default DragAndDrop
DragAndDrop.jsx
Create the state and layout for the DragAndDrop component

The cards will be stored in the cards state. Each card comes with three properties:

  • id: The ID of the card. This needs to be unique as we are going to identify which card is being dragged based on its ID.
  • name: The name of the card that will be displayed on the screen.
  • state: The state of the card that defines which column it belongs to. In this example, it can be one of todo, ip, or done.

To display the correct cards inside each column, we can filter them based on their state before looping through them using a map. The same structure applies to the other two columns.

πŸ” Login to get access to the full source code in one piece. With CSS and TypeScript types included.

Adding drag-and-drop event listeners

In order to work with Drag & Drop, we will need some additional event listeners on our columns and cards. Extend each column and card with the following props:

Copied to clipboard! Playground
const DragAndDrop = () => {
    const [cards, setCards] = useState(initialState)

    const dragEnter = event => { ... }
    const dragLeave = event => { ... }
    const drag = event => { ... }
    const drop = event => { ... }
    const allowDrop = event => { ... }

    return (
        <main className="board">
            <div
                className="column column-todo"
                data-column="todo"
                onDragEnter={dragEnter}
                onDragLeave={dragLeave}
                onDragOver={allowDrop}
                onDrop={drop}
            >
                <h2>Todo</h2>
                {cards.filter(card => card.state === 'todo').map(todo => (
                    <article key={todo.id} className="card" draggable="true" onDragStart={drag} data-id={todo.id}>
                        <h3>{todo.name}</h3>
                    </article>
                ))}
            </div>
            ...
        </main>
    )
}
    
export default DragAndDrop
DragAndDrop.jsx
Extend the DragAndDrop component with the highlighted parts

You can leave the referenced functions empty for now.

The other two columns will get the exact same attributes. There is a lot going on, so let's break down the purpose of each prop in order:

  • Line 4: The dragEnter function will be called when the onDragEnter event listener is triggered on line:15. This event will be fired when a draggable element enters a valid drop area.
  • Line 5: The dragLeave function will be called when the onDragLeave event listener is triggered on line:16. This event will be fired when a draggable element leaves a valid drop area.
  • Line 6: The drag function will be called when the onDragStart event listener is triggered on line:22. This event will be fired when a user starts to drag an element. This event listener needs to be on the article as it will be the draggable element. On the other hand, dragEnter and dragLeave need to be on the drop area which will be the .column.
  • Line 7: The drop function will be called when the onDrop event listener is triggered on line:18. This event will be fired when we drop an element on a valid drop target. This is where we will handle dropping functionality.
  • Line 8: The allowDrop function will be called when the onDragOver event listener is triggered on lin:17. Whenever we drag an item over a drop area, this will be fired every few hundred milliseconds. Since this is called a lot, it’s important that we avoid placing calculation-heavy functions here. We can also debounce the function to further reduce the number of calls.
πŸ” Login to get access to the full source code in one piece. With CSS and TypeScript types included.

We also have data-column and data-id attributes present on the .column and article elements. These will be used for carrying important data during dragging. Lastly, don't forget to set draggable to true on the element that you need to drag. This sets whether an element will be draggable.

mouse effect with CSS
The grabbing mouse effect created with CSS

At this stage, the elements are now draggable. With the help of some CSS, we can set up different cursor icons to indicate when we grab an item.

Copied to clipboard! Playground
.card {
    cursor: grab;
}

.card:active {
    cursor: grabbing;
}
styles.css
Change the cursor property when the element is active.

Dragging Elements

Now that we have everything set up, we can start writing the functionality for dragging elements. First, you should always provide visual feedback to the user about an action. When the user drags an item, we want to indicate that.

Animation when dragging
Animation when dragging elements

We can do this by adding an extra class to the element when we move it. We can achieve this with an additional CSS class. To translate this into React, add the following useEffect hook to the component that will attach the dragstart and dragend events to the document:

Copied to clipboard! Playground
useEffect(() => {
    document.addEventListener('dragstart', dragStart)
    document.addEventListener('dragend', dragEnd)

    return () => {
        document.removeEventListener('dragstart', dragStart)
        document.removeEventListener('dragend', dragEnd)
    }
}, [])
DragAndDrop.jsx
Return a function from useEffect to remove the listeners on unmount

To recap:

  • dragstart: This event will be fired when the user starts dragging an item.
  • dragend: This event will be fired when the user stops dragging the item.

So what goes inside the dragStart and dragEnd functions? We simply want to toggle the .dragging class shown in the previous GIF. Extend the dragStart and dragEnd functions according to the following:

Copied to clipboard! Playground
const dragStart = event => {
    if (event.target.className.includes('card')) {
        event.target.classList.add('dragging')
    }
}

const dragEnd = event => {
    if (event.target.className.includes('card')) {
        event.target.classList.remove('dragging')
    }
}
DragAndDrop.jsx
Toggle classes using dragstart and dragend event listeners
giving visual feedback to the user when dragging items
Whenever an item is dragged, the dragging class is applied

To achieve this effect in CSS, we need to apply a transition and set the opacity to 50% and reduce the scale to 80%:

Copied to clipboard! Playground
.card {
    transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
}

.card.dragging {
    opacity: .5;
    transform: scale(.8);
}
styles.css
Animating the cards with CSS

What makes drag and drop work?

While here, it is time to set up the drag function which will be fired on every onDragStart event. The sole responsibility of this function is to set up some data that we can later use when the item is dropped. Add the following to the drag function:

Copied to clipboard!
const drag = event => {
    event.dataTransfer.setData('text/plain', event.currentTarget.dataset.id)
}
DragAndDrop.jsx
Implement the drag function

The dataTransfer object can hold data about the dragged object. The setData method takes two parameters: a mime type and a payload. Here we want to store the data-id attribute of the dragged element.

Looking to improve your skills? Check out our interactive course to master React from start to finish.
Master Reactinfo Remove ads

Dropping Elements

Now we can move onto dropping the items. Again, let’s start by providing visual feedback to the user when they drag the card over a column:

Copied to clipboard! Playground
const dragEnter = event => {
    event.currentTarget.classList.add('drop')
}

const dragLeave = event => {
    event.currentTarget.classList.remove('drop')
}
DragAndDrop.jsx
Add the above to dragEnter and dragLeave

This time, we want to use the onDragEnter and onDragLeave events. Very similar to dragstart and dragend, but they have different purposes:

  • onDragEnter: This event will be fired when a draggable element enters a valid drop area.
  • onDragLeave: This event will be fired when a draggable element leaves a valid drop area.
Highlighting columns when dragging items over it
Dragging the item over a column will highlight it with a dashed border

Make sure you remove the pointer-events from the cards in CSS. This will ensure that the column receives all events and get the dashed border, even if we drag the card over another card.

All that’s left to do is to actually add the items to the new column. For this, we will need to implement the drop and allowDrop functions:

Copied to clipboard!
const allowDrop = event => {
    event.preventDefault()
}
DragAndDrop.jsx
Add the allowDrop function

The allowDrop function is used for preventing the default behavior of the browser so we can actually drop the item into the column. This can be achieved with a simple event.preventDefault call. As for the drop function, we need to update the cards state with the help of the dataTransfer object:

Copied to clipboard! Playground
const drop = event => {
    const column = event.currentTarget.dataset.column
    const id = Number(event.dataTransfer.getData('text/plain'))

    event.currentTarget.classList.remove('drop')

    event.preventDefault()

    const updatedState = cards.map(card => {
        if (card.id === id) {
            card.state = column
        }

        return card
    })

    setCards(updatedState)
}
DragAndDrop.jsx
Add the drop function

In the drop function, we need to grab the column type using dataset.column. This will be either todo, ip, or done.

Make sure that the data-column attributes match one of the values of card.state.

Here we can also retrieve the data-id attribute from the dataTransfer object using the getData method. We are going to use these values to update the state of the cards in our useState hook. But before doing so, there are two additional things we want to sort out with our event:

  • Remove any default browser actions using event.preventDefault.
  • Remove the .drop class which is responsible for adding a dashed border.

The last step is to update the state by using a map. We can use the id from the dataTransfer object to update the proper card.state to the column variable, essentially telling React in which column should the card be in. To recap the purpose of each variable:

VariableAttributePurpose
iddata-id on .cardThe ID of the card that is being dragged
columndata-column on .columnThe column where the card is dragged to
Dragging and dropping items
Drag and drop implemented in React

Summary

And that’s it. You’ve just created your very first Kanban board with drag-and-drop functionality In React! If you would like to learn more about the native drag-and-drop API, make sure to check out the official docs of MDN web docs. It includes all available drag events that you can use in different phases of the drag and drop lifecycle.

And if you would like learn more about React, check out our previous tutorial below on how to make a responsive, animated navbar. Have you used the Drag and Drop API before? Let us know your thoughts in the comments down below! Thank you for taking the time to read this article, happy coding! πŸ‘¨β€πŸ’»

5 Best Practices for Clean JavaScript
  • twitter
  • facebook
React
Did you find this page helpful?
πŸ“š More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access 100+ interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Prepare for technical interviews
Become a Pro

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.