Building a drag-and-drop Kanban board is a rite of passage for React developers. Trello made this interface famous, and Jira made it standard. But under the hood, managing the state of moving items across different columns is notoriously tricky.

In this tutorial, we will build a fully functional, production-ready Kanban board in React. We will track job applications moving through different stages: Bookmarked, Applied, Interview, Offer, and Rejected. By the end, you will understand the physics of drag-and-drop, immutable state updates, and how to persist this data reliably.

What is a Kanban Board?

Skip reading — generate your PPT now

Our AI creates professional, editable slides from any topic in under 30 seconds. Free, no signup.

Generate Free PPT →

Imagine a whiteboard with three vertical columns drawn on it: "To Do", "Doing", and "Done". You have a stack of sticky notes. You write a task on a sticky note and place it in the "To Do" column. When you start working on it, you physically peel the sticky note off the board and stick it into the "Doing" column.

That is a Kanban board. The word "Kanban" comes from Japanese, roughly translating to "visual signal" or "card". Toyota invented the system in the 1940s to optimize manufacturing. Today, software teams use it to track features, and job seekers use it to track interviews.

In software, translating a physical sticky note into code requires handling complex browser events. We need to track where the mouse is clicked, how the mouse moves, when the item intersects with a new column, and where it is finally dropped. Doing this from scratch using the native HTML5 Drag and Drop API is a painful experience involving ghost images and browser inconsistencies. That is why we use libraries.

Why @hello-pangea/dnd?

For years, the gold standard for drag-and-drop in React was react-beautiful-dnd, built by Atlassian. It was famous for its buttery smooth animations, natural physics, and screen-reader accessibility right out of the box.

However, Atlassian stopped actively maintaining it. As React evolved with React 18 and strict mode, the original library started throwing warnings and breaking.

Enter @hello-pangea/dnd. It is a drop-in replacement and maintained fork of react-beautiful-dnd. It keeps everything that made the original great—the physics, the accessibility, the simple API—but updates it to work flawlessly with modern React. While @dnd-kit is another popular choice today, @hello-pangea/dnd remains superior if you specifically want to build list-based interfaces like a Kanban board without writing hundreds of lines of boilerplate.

Step-by-Step: Building the Board

Let us write the code. We will start by defining our data structure. The biggest mistake developers make with drag-and-drop is structuring their state poorly.

1. The Data Structure

A Kanban board consists of columns, and columns contain items (tasks). We need a structure that allows fast lookups. We will use an object to store our columns, where the keys are the column IDs.

// types.ts
export interface JobApplication {
  id: string;
  company: string;
  role: string;
  date: string;
}

export interface ColumnData {
  id: string;
  title: string;
  items: JobApplication[];
}

export interface BoardState {
  [key: string]: ColumnData;
}

// Initial State
export const initialData: BoardState = {
  bookmarked: {
    id: 'bookmarked',
    title: 'Bookmarked',
    items: [
      { id: 'job-1', company: 'Stripe', role: 'Frontend Engineer', date: 'Oct 1' },
      { id: 'job-2', company: 'Vercel', role: 'React Developer', date: 'Oct 2' }
    ]
  },
  applied: {
    id: 'applied',
    title: 'Applied',
    items: []
  },
  interview: {
    id: 'interview',
    title: 'Interviewing',
    items: []
  }
};

Notice that every item has a unique string id. This is non-negotiable. The drag-and-drop library uses these IDs to track DOM elements. If you use array indices as IDs, your board will glitch when items change order.

2. Setting up DragDropContext, Droppable, and Draggable

The library gives us three main components:

  • DragDropContext: The wrapper that provides the drag state to your app. It takes an onDragEnd callback.
  • Droppable: An area where items can be dropped (our columns).
  • Draggable: The items themselves that can be picked up and moved (our job cards).

Here is how we assemble the board layout:

import React, { useState } from 'react';
import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd';
import { initialData, BoardState } from './types';

export default function KanbanBoard() {
  const [columns, setColumns] = useState<BoardState>(initialData);

  const onDragEnd = (result: DropResult) => {
    // We will implement this in the next step
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div className="flex gap-4 p-6 overflow-x-auto min-h-screen bg-gray-50">
        {Object.values(columns).map((column) => (
          <div key={column.id} className="flex flex-col bg-gray-200 rounded-lg w-80 shrink-0">
            <h3 className="p-4 font-bold text-gray-700">{column.title}</h3>
            
            <Droppable droppableId={column.id}>
              {(provided, snapshot) => (
                <div
                  ref={provided.innerRef}
                  {...provided.droppableProps}
                  className={`flex-1 p-4 transition-colors ${
                    snapshot.isDraggingOver ? 'bg-gray-300' : ''
                  }`}
                >
                  {column.items.map((item, index) => (
                    <Draggable key={item.id} draggableId={item.id} index={index}>
                      {(provided, snapshot) => (
                        <div
                          ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                          className={`mb-3 p-4 bg-white rounded shadow-sm border border-gray-200 ${
                            snapshot.isDragging ? 'shadow-lg ring-2 ring-blue-500' : ''
                          }`}
                        >
                          <p className="font-semibold text-gray-900">{item.company}</p>
                          <p className="text-sm text-gray-500">{item.role}</p>
                        </div>
                      )}
                    </Draggable>
                  ))}
                  {provided.placeholder}
                </div>
              )}
            </Droppable>

          </div>
        ))}
      </div>
    </DragDropContext>
  );
}

Notice the render props pattern. Droppable and Draggable expect a function as their child. The provided object contains the refs and DOM props needed to make the drag physics work. We spread these onto our standard HTML divs. The provided.placeholder ensures that the column does not shrink when you drag an item out of it.

3. Handling onDragEnd

The visual part is done, but if you drag an item and let go, it snaps right back to where it started. We need to update our React state to reflect the drop.

The onDragEnd function receives a result object. It contains the source (where the item came from) and the destination (where it was dropped). We have to handle three scenarios:

  1. The user dropped the item outside a droppable area (do nothing).
  2. The user reordered the item within the same column.
  3. The user moved the item to a completely different column.
const onDragEnd = (result: DropResult) => {
  const { source, destination } = result;

  // 1. Dropped outside the list
  if (!destination) return;

  const sourceCol = columns[source.droppableId];
  const destCol = columns[destination.droppableId];

  // 2. Reordering within the same column
  if (sourceCol === destCol) {
    const newItems = Array.from(sourceCol.items);
    // Remove from old index
    const [movedItem] = newItems.splice(source.index, 1);
    // Insert at new index
    newItems.splice(destination.index, 0, movedItem);

    setColumns({
      ...columns,
      [sourceCol.id]: {
        ...sourceCol,
        items: newItems,
      },
    });
    return;
  }

  // 3. Moving from one column to another
  const sourceItems = Array.from(sourceCol.items);
  const destItems = Array.from(destCol.items);

  // Remove from source column
  const [movedItem] = sourceItems.splice(source.index, 1);
  // Insert into destination column
  destItems.splice(destination.index, 0, movedItem);

  setColumns({
    ...columns,
    [sourceCol.id]: {
      ...sourceCol,
      items: sourceItems,
    },
    [destCol.id]: {
      ...destCol,
      items: destItems,
    },
  });
};

This is immutable state management in practice. We never mutate the existing arrays directly. Instead, we create a copy of the array using Array.from(), splice the array to remove and insert the item, and then spread the updated columns back into our state object.

4. Persisting Data to Firestore

If the user refreshes the page right now, their board resets to the initial state. You need persistence. While beginners can start with localStorage, a real application requires a database.

The trick is to use an Optimistic UI update. You update the React state immediately so the user sees the card drop seamlessly. Then, in the background, you fire an asynchronous request to save the new state to your database.

If you are using Firebase Firestore, your onDragEnd function would dispatch the update like this:

// Inside onDragEnd, after calling setColumns(...)
const updateFirestore = async () => {
  try {
    await updateDoc(doc(db, 'boards', userId), {
      columns: newColumnsState // The new state we just calculated
    });
  } catch (error) {
    console.error("Failed to save board layout", error);
    // Revert setColumns back to previous state if API fails
  }
};
updateFirestore();

Real-World Features: PPT Maker's Application Vault

At PPT Maker, we built the Application Vault using this exact architecture. But a production Kanban board requires more than just gray rectangles.

Color-coded statuses: We render specific accent colors based on the column ID. The "Offer" column gets a green border, while "Rejected" gets subtle red styling. This visual hierarchy helps users process information instantly.

Rich Card Components: Our Draggable cards are not simple divs. They contain company logos, salary ranges, and quick-action buttons. To prevent drag-and-drop from interfering with button clicks, you must ensure the dragHandleProps are only attached to the card's header or a specific drag icon, rather than the entire card wrapper.

Mobile Responsiveness: Horizontal scrolling Kanban boards are notoriously difficult to use on mobile devices. In the Application Vault, we detect screen width. On desktop, users get the horizontal Kanban view. On mobile, the columns stack vertically as accordions, and users can move cards using a native select dropdown menu instead of dragging.

Connecting the Context Bridge

The real power of tracking your job search in a Kanban board is what you can do with that structured data.

When you move a card into the "Applied" column, you probably generated a tailored resume first. The Application Vault talks directly to our ATS Resume Checker. The company data flows seamlessly between tools.

When you move a card into the "Interview" column, the Vault triggers a notification suggesting you prepare. It feeds the specific job description and company profile from that card directly into our Mock Interview engine, generating tailored interview questions for that exact role. And if you need to follow up with a recruiter, the data is already queued up for the Cover Letter AI to write a highly contextual post-interview thank you email.

Conclusion

Building a drag-and-drop Kanban board is a fantastic way to master React state and immutable data structures. By using @hello-pangea/dnd, you avoid the nightmares of the native HTML5 drag API and ship an accessible, buttery-smooth experience.

If you want to see this architecture in action, stop tracking your job applications in messy spreadsheets. Try our Application Vault. It is completely free, built for speed, and integrates natively with our entire suite of AI career tools to give you an unfair advantage in your job hunt.

Frequently Asked Questions

What is the best drag-and-drop library for React in 2026?

For most applications, @hello-pangea/dnd remains the top choice due to its robust Atlassian-backed heritage, accessibility features, and ease of use. However, @dnd-kit is an excellent modular alternative if you need highly customized drag interactions or are building a design tool.

Is @hello-pangea/dnd the same as react-beautiful-dnd?

It is a maintained fork. Atlassian officially stopped maintaining react-beautiful-dnd a few years ago. The community, led by Pangea, took over the project to ensure it works perfectly with modern React versions, including React 18 and React 19 strict mode.

How do I persist Kanban board state to a database?

You should trigger your database update inside the onDragEnd function. Calculate the new state, update the UI optimistically, and then send the new column arrays (or order indexes) to your database backend like Firebase Firestore, Supabase, or PostgreSQL.

Can I use this Kanban board on mobile devices?

Yes. @hello-pangea/dnd has built-in touch sensor support. However, from a UX perspective, vertical scrolling columns can be tricky on narrow screens. It is often best to implement a horizontal swipe view for columns on mobile, or provide a fallback dropdown menu to move items without dragging.

How does PPT Maker's Application Vault work?

The Application Vault uses a highly optimized drag-and-drop Kanban interface connected to Firestore. It allows users to track job applications across different stages, and it seamlessly connects with our Resume Checker and Cover Letter AI to give you context-aware application generation.

What is the difference between @dnd-kit and @hello-pangea/dnd?

@dnd-kit is a lightweight, modular, and unopinionated drag-and-drop toolkit. It requires more boilerplate but offers ultimate flexibility. @hello-pangea/dnd is highly opinionated, specifically designed for lists and boards. It gives you accessibility and physics right out of the box with much less setup.

Chandrakant Kelgire — BCA Student & Product Builder

Chandrakant Kelgire is a BCA student and the creator of Student Suite. He writes about AI tools, productivity hacks, and modern presentation techniques to help students and professionals save time and work smarter.