DEV Community

Cover image for I Built a Drag-and-Drop Kanban Board with Vanilla JavaScript
Praise Oluleye
Praise Oluleye

Posted on

I Built a Drag-and-Drop Kanban Board with Vanilla JavaScript

If you've ever wanted to understand how drag-and-drop actually works under the hood — no libraries, no frameworks, just the browser — building a Kanban board is a great way to find out. Here's how I built one using plain HTML, CSS, and JavaScript.

The Idea

A Kanban board is one of those projects that looks simple on the surface but forces you to actually understand the DOM: creating elements, listening for events, and moving nodes around dynamically. I wanted three columns — To Do, In Progress, and Done — with cards you can drag between them.

Setting Up the Structure

The HTML is just a .board container holding three .list columns, each with a heading and a few .card elements marked draggable="true":

<div class="list" id="list1">
  <h2>To do</h2>
  <div class="card" draggable="true" id="card1">Wash dishes</div>
  <div class="card" draggable="true" id="card2">Buy groceries</div>
</div>
Enter fullscreen mode Exit fullscreen mode

That draggable="true" attribute is doing a lot of quiet work — it's what unlocks the HTML5 Drag and Drop API on that element.

Making It Draggable

The core logic comes down to a handful of events split between the cards and the lists.

On each card:

card.addEventListener("dragstart", dragStart);
card.addEventListener("dragend", dragEnd);

function dragStart(e) {
  e.dataTransfer.setData("text/plain", this.id);
}
Enter fullscreen mode Exit fullscreen mode

When a drag starts, I stash the card's id in the dataTransfer object. That's the only "state" I need to track — everything else can be looked up from the DOM.

On each list:

list.addEventListener("dragover", dragOver);
list.addEventListener("dragenter", dragEnter);
list.addEventListener("dragleave", dragLeave);
list.addEventListener("drop", dragDrop);

function dragOver(e) {
  e.preventDefault(); // required, or drop never fires
}

function dragDrop(e) {
  const id = e.dataTransfer.getData("text/plain");
  const card = document.getElementById(id);
  this.appendChild(card);
  this.classList.remove("over");
}
Enter fullscreen mode Exit fullscreen mode

The one gotcha that trips a lot of people up: dragover must call preventDefault(), or the drop event will never fire. Browsers block dropping by default, and that call is what tells the browser "yes, this element accepts drops."

Once the card is dropped, appendChild() handles the rest — since a card is a single DOM node, moving it to a new parent just relocates it, no cloning or re-rendering needed.

A Bit of Polish

I added a .over class that toggles on dragenter/dragleave to give the target list a subtle highlight while you're dragging over it — small detail, but it makes the board feel responsive instead of static.

.list.over {
  background-color: #d1d3d3;
}
Enter fullscreen mode Exit fullscreen mode

What I'd Add Next

This is intentionally a minimal version, but a few natural next steps:

  • Persistence — save board state to localStorage so cards don't reset on refresh
  • Add/edit/delete cards — right now the cards are hardcoded in the HTML
  • Touch support — the native Drag and Drop API doesn't work great on mobile, so a touch-based fallback would help

Wrap-up

No libraries, no build tools — just the Drag and Drop API and a bit of CSS. It's a solid project if you want a deeper feel for how the DOM handles events and element movement, and it's satisfying to drag a card across the screen and watch it just... work.

Code's on GitHub if you want to poke around or build on it: github.com/Praise-04-dev

Would love feedback — especially if you've built something similar and handled persistence or touch support differently!

Top comments (0)