Create React Drag and Drop Component

Faraz

By Faraz - Last Updated:

Learn how to build a simple React drag and drop component. Step-by-step guide with clean UI, fast setup, and beginner-friendly code.


create-react-drag-and-drop-component.webp

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

DND Live Demo ⟶

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 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Components

Please allow ads on our site🥺