Learn how to build a simple React drag and drop component. Step-by-step guide with clean UI, fast setup, and beginner-friendly code.
Want to build a modern drag-and-drop component in your React app? With the help of dnd-kit, you can create fast and flexible drag-and-drop interfaces.
In this guide, you’ll learn how to use React + dnd-kit with Vite to make a drag and drop component. The setup is easy, and the results are clean and professional.
Let’s get started!
Setup Environment
Let's start by setting up our development environment using Vite, which is faster than Create React App.
Step 1: Create Vite React App
npm create vite@latest react-dndkit-app -- --template react
cd react-dndkit-app
Step 2: Install dnd-kit
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/modifiers
Step 3: Run Your App
npm install
npm run dev
Your Vite React app is ready to use.
Step-by-Step: Create a Drag and Drop Component
Step 1: Create File Structure
Create a folder inside src:
import React, { useState, useMemo, useEffect, useRef } from 'react';
import {
DndContext,
closestCorners,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
arrayMove,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
// Card Component
const Card = ({ id, content, className = '' }) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 10 : 0, // Ensure dragged item is on top
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={`
bg-white dark:bg-gray-700
p-4 mb-3 rounded-lg shadow-md
cursor-grab active:cursor-grabbing
transition-all duration-200 ease-out
hover:shadow-lg hover:translate-y-[-2px]
ring-1 ring-gray-100 dark:ring-gray-600
${isDragging ? 'opacity-70 border-2 border-indigo-500 transform scale-105 shadow-xl' : ''}
${className}
`}
>
<p className="text-gray-800 dark:text-gray-100 text-sm font-medium">{content}</p>
</div>
);
};
// Column Component
const Column = ({ id, title, cards, onAddCard, isOverContainer }) => {
const { setNodeRef } = useSortable({
id,
data: {
type: 'Container',
},
});
const [showAddCardInput, setShowAddCardInput] = useState(false);
const [newCardContent, setNewCardContent] = useState('');
const inputRef = useRef(null);
useEffect(() => {
if (showAddCardInput) {
inputRef.current?.focus();
}
}, [showAddCardInput]);
const handleSaveCard = () => {
if (newCardContent.trim()) {
onAddCard(id, newCardContent.trim());
setNewCardContent('');
setShowAddCardInput(false);
}
};
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
handleSaveCard();
}
if (event.key === 'Escape') {
setNewCardContent('');
setShowAddCardInput(false);
}
};
return (
<div
ref={setNodeRef}
className={`
w-full md:w-80 lg:w-96
flex-shrink-0
bg-gradient-to-br from-white to-gray-50 dark:from-gray-800 dark:to-gray-850
rounded-xl shadow-xl p-5 m-2
ring-1 ring-gray-200 dark:ring-gray-700
transition-all duration-200 ease-out
${isOverContainer ? 'border-2 border-dashed border-indigo-500 bg-opacity-80 scale-[1.01]' : ''}
flex flex-col
`}
>
<h2 className="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 pb-3 border-b border-gray-200 dark:border-gray-600">
{title} ({cards.length})
</h2>
<div className="flex-grow min-h-[50px]"> {/* Ensure column has a minimum height for dropping */}
<SortableContext items={cards.map(card => card.id)} strategy={verticalListSortingStrategy}>
{cards.map(card => (
<Card key={card.id} id={card.id} content={card.content} />
))}
</SortableContext>
{showAddCardInput && (
<div className="mt-3">
<textarea
ref={inputRef}
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded-md bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-indigo-500 focus:border-indigo-500 text-sm"
rows="3"
placeholder="Enter card content..."
value={newCardContent}
onChange={(e) => setNewCardContent(e.target.value)}
onKeyDown={handleKeyDown}
></textarea>
<div className="flex justify-end space-x-2 mt-2">
<button
onClick={handleSaveCard}
className="py-1.5 px-3 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-md shadow-sm transition duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-opacity-75"
>
Add Card
</button>
<button
onClick={() => {
setNewCardContent('');
setShowAddCardInput(false);
}}
className="py-1.5 px-3 bg-gray-300 hover:bg-gray-400 text-gray-800 text-sm font-semibold rounded-md shadow-sm transition duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-opacity-75 dark:bg-gray-600 dark:text-gray-100 dark:hover:bg-gray-500"
>
Cancel
</button>
</div>
</div>
)}
</div>
{!showAddCardInput && (
<button
onClick={() => setShowAddCardInput(true)}
className="mt-4 w-full py-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-lg shadow-md transition duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-opacity-75 transform hover:scale-[1.01]"
>
+ Add New Card
</button>
)}
</div>
);
};
// Main App Component
export default function App() {
const [columns, setColumns] = useState({
'todo': {
id: 'todo',
title: 'To Do',
cards: [
{ id: '1', content: 'Design homepage layout' },
{ id: '2', content: 'Implement user authentication' },
{ id: '3', content: 'Set up database schema' },
],
},
'in-progress': {
id: 'in-progress',
title: 'In Progress',
cards: [
{ id: '4', content: 'Develop API' },
{ id: '5', content: 'Build front-end components' },
],
},
'done': {
id: 'done',
title: 'Done',
cards: [
{ id: '6', content: 'Project initialization' },
{ id: '7', content: 'Basic routing setup' },
],
}
});
const [activeId, setActiveId] = useState(null);
const [overContainerId, setOverContainerId] = useState(null);
// Get active card for DragOverlay
const activeCard = activeId ? Object.values(columns).flatMap(col => col.cards).find(card => card.id === activeId) : null;
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
// Function to add a new card
const handleAddCard = (columnId, content) => {
const newCardId = `card-${Date.now()}`;
setColumns(prevColumns => ({
...prevColumns,
[columnId]: {
...prevColumns[columnId],
cards: [...prevColumns[columnId].cards, { id: newCardId, content: content }],
},
}));
};
// Handle drag start
const handleDragStart = (event) => {
setActiveId(event.active.id);
setOverContainerId(null); // Reset on drag start
};
// Handle drag over (when an item is dragged over another sortable item or container)
const handleDragOver = (event) => {
const { active, over } = event;
// If no active or over element, do nothing
if (!active || !over) return;
// Determine the ID of the container where the active item started
const activeContainerId = Object.values(columns).find(col => col.cards.some(card => card.id === active.id))?.id;
// Determine the ID of the container where the active item is currently over
// It could be over another card (then get its container ID) or directly over a container
const overContainerIdResolved = Object.values(columns).find(col => col.cards.some(card => card.id === over.id))?.id || (over.data.current?.type === 'Container' ? over.id : null);
// Update overContainerId for visual feedback on columns
setOverContainerId(overContainerIdResolved);
// Logic for moving within the same container or between containers
if (activeContainerId && overContainerIdResolved && activeContainerId !== overContainerIdResolved) {
setColumns((prevColumns) => {
// Create mutable copies of cards arrays
const sourceCards = [...prevColumns[activeContainerId].cards];
const destinationCards = [...prevColumns[overContainerIdResolved].cards];
const activeIndex = sourceCards.findIndex((card) => card.id === active.id);
const overIndex = destinationCards.findIndex((card) => card.id === over.id);
// Remove the card from the source column
const [movedCard] = sourceCards.splice(activeIndex, 1);
// Add the card to the destination column
if (overIndex !== -1) {
// If over another card in the destination, insert before/after it
destinationCards.splice(overIndex, 0, movedCard);
} else {
// If over the container itself (and not a specific card), add to the end
destinationCards.push(movedCard);
}
return {
...prevColumns,
[activeContainerId]: {
...prevColumns[activeContainerId],
cards: sourceCards,
},
[overContainerIdResolved]: {
...prevColumns[overContainerIdResolved],
cards: destinationCards,
},
};
});
}
};
// Handle drag end (finalize the drag operation)
const handleDragEnd = (event) => {
const { active, over } = event;
// Reset active ID and over container ID
setActiveId(null);
setOverContainerId(null);
// If no over element, do nothing
if (!active || !over) return;
const activeContainerId = Object.values(columns).find(col => col.cards.some(card => card.id === active.id))?.id;
const overContainerIdResolved = Object.values(columns).find(col => col.cards.some(card => card.id === over.id))?.id || (over.data.current?.type === 'Container' ? over.id : null);
if (!activeContainerId || !overContainerIdResolved) return;
// If dragging within the same container, finalize position
if (activeContainerId === overContainerIdResolved) {
// We need to re-evaluate indices in handleDragEnd as positions might have shifted during dragOver
setColumns((prevColumns) => {
const currentCards = [...prevColumns[activeContainerId].cards];
const oldIndex = currentCards.findIndex(card => card.id === active.id);
const newIndex = currentCards.findIndex(card => card.id === over.id);
if (oldIndex !== newIndex) {
return {
...prevColumns,
[activeContainerId]: {
...prevColumns[activeContainerId],
cards: arrayMove(currentCards, oldIndex, newIndex),
},
};
}
return prevColumns; // No change needed if indices are the same
});
}
// For inter-container drags, the move was already handled in handleDragOver for smoother visual feedback.
// No additional state update is required here for those cases.
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-100 to-gray-300 dark:from-gray-950 dark:to-gray-800 p-8 font-inter">
<h1 className="text-4xl font-extrabold text-center text-gray-900 dark:text-white mb-10 drop-shadow-lg">
React Drag and Drop Component
</h1>
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="flex flex-col md:flex-row justify-center items-start md:items-stretch space-y-6 md:space-y-0 md:space-x-6 max-w-7xl mx-auto overflow-x-auto pb-4">
{Object.values(columns).map((column) => (
<Column
key={column.id}
id={column.id}
title={column.title}
cards={column.cards}
onAddCard={handleAddCard}
isOverContainer={overContainerId === column.id}
/>
))}
</div>
<DragOverlay>
{activeCard ? (
<Card
id={activeCard.id}
content={activeCard.content}
className="!shadow-2xl !ring-4 !ring-indigo-500 !bg-gradient-to-br !from-purple-300 !to-indigo-300 dark:!from-purple-700 dark:!to-indigo-700"
/>
) : null}
</DragOverlay>
</DndContext>
</div>
);
}
// Ensure Tailwind CSS is loaded (assuming standard CDN setup)
const tailwindScript = document.createElement('script');
tailwindScript.src = 'https://cdn.tailwindcss.com';
document.head.appendChild(tailwindScript);
// Optional: Inter font import for modern look
const interFontLink = document.createElement('link');
interFontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap';
interFontLink.rel = 'stylesheet';
document.head.appendChild(interFontLink);
const styleTag = document.createElement('style');
styleTag.innerHTML = `
body {
font-family: 'Inter', sans-serif;
}
`;
document.head.appendChild(styleTag);
Step 3: Use DragDrop in App.jsx
Replace the default code in App.jsx with:
import DragDrop from './components/DragDrop';
function App() {
return (
<>
<DragDrop />
</>
)
}
export default App
Remove Unused CSS Files (src/assets/App.css & src/assets/Index.css)
Since we're using 100% Tailwind CSS via CDN, you can safely delete the default App.css and Index.css files to keep your project clean and lightweight.
Run Your App
Go to your terminal:
npm run dev
Open http://localhost:5173/ and see your drag and drop component.
Conclusion
Congrats! You just created a beautiful drag and drop component in React using dnd-kit and Vite.
Key Benefits:
- Fast setup with Vite
- Modern drag-and-drop using dnd-kit
- Clean and responsive UI
- Easy to expand for advanced usage
That’s a wrap!
I hope you enjoyed this article
Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee.
And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!
Thanks!
Faraz 😊

