webdevpuneet.com

Interact.js Drag-Drop Kanban — Cross-Column Card Board Snippet

Interact.js Drag-Drop Kanban · Dashboards · Plain HTML, CSS & JS · Live preview

CategoryDashboards

Copy & paste

Source Code

Requires
<div class="idk-stage">
  <div class="idk-head">
    <span class="idk-tag">interact.js · draggable + dropzone</span>
    <h2>Drag-Drop Board</h2>
    <p>Drag a card between columns — the target column highlights while a valid card hovers over it.</p>
  </div>
  <div class="idk-board" id="idkBoard">
    <div class="idk-col" data-col="todo">
      <div class="idk-col-head"><span class="idk-dot" style="background:#f87171"></span>To Do<span class="idk-count"></span></div>
      <div class="idk-drop" data-col="todo"></div>
    </div>
    <div class="idk-col" data-col="doing">
      <div class="idk-col-head"><span class="idk-dot" style="background:#fbbf24"></span>In Progress<span class="idk-count"></span></div>
      <div class="idk-drop" data-col="doing"></div>
    </div>
    <div class="idk-col" data-col="done">
      <div class="idk-col-head"><span class="idk-dot" style="background:#4ade80"></span>Done<span class="idk-count"></span></div>
      <div class="idk-drop" data-col="done"></div>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Cross-column dragging
Cards move between three independent dropzone columns using interact.js draggable + dropzone.
Live drop-target highlight
ondragenter/ondragleave toggle a distinct visual state only while a valid card is hovering.
Overlap threshold
overlap: 0.4 avoids flicker between adjacent columns near a shared boundary.
Node re-parenting on drop
The actual dragged element moves via appendChild, preserving its existing draggable wiring.
Transform-reset on drag end
The drag offset is cleared on every drag end so a dropped card lands cleanly in its new spot.
Data model kept in sync
Each card's column field updates alongside the DOM move, keeping counts and future renders correct.
Elevated active card
The dragged card gets a raised z-index and dimmed opacity so it reads clearly above siblings.
Responsive board
Three columns collapse to a single stacked column under 640px via one media query.

About this UI Snippet

Interact.js Drag-Drop Kanban — Draggable and Dropzone Working Together

Screenshot of the Interact.js Drag-Drop Kanban snippet rendered live

A kanban board needs two interact.js concerns to cooperate: the card itself must be draggable, and each column must be a dropzone that knows when a valid card is hovering over it and what to do when one lands. Neither API knows about the other directly — they communicate entirely through events interact.js fires on both sides of the interaction.

The card side: draggable with a transform-only move

Each card gets the same data-x/data-y accumulation pattern used for free-floating drag: event.dx/event.dy deltas are added onto attributes and applied via transform, never touching layout properties. Two listeners bracket the drag:

start: function (event) { event.target.classList.add('idk-dragging'); event.target.style.zIndex = 50; }

raises the card above its siblings and dims it slightly so it's visually clear which card is being moved, and

end: function (event) { ...; target.style.transform = ''; target.setAttribute('data-x', 0); target.setAttribute('data-y', 0); }

resets the drag transform to zero regardless of whether the drop succeeded. This matters because a successful drop re-parents the actual DOM node into the new column (see below) rather than rebuilding the board — if the transform offset weren't cleared, the card would land in its new column visually shifted by however far it had been dragged.

The column side: dropzone and its three events

interact('.idk-drop').dropzone({ accept: '.idk-card', overlap: 0.4, ondragenter, ondragleave, ondrop }) turns every column's drop area into a target that reacts to draggables matching accept. overlap: 0.4 means a card must overlap a dropzone by at least 40% of its area before interact.js considers it "over" that zone — this avoids ambiguous flickering between two adjacent columns when a card is only barely crossing a boundary.

ondragenter and ondragleave fire purely for the highlight, independent of whether the card is ultimately dropped there:

ondragenter: function (event) { event.target.classList.add('idk-over'); } ondragleave: function (event) { event.target.classList.remove('idk-over'); }

event.target inside a dropzone listener is the dropzone element (the column), not the card — this is the opposite of what draggable's listeners give you, and is the detail most likely to trip someone up moving between the two APIs.

The actual drop: event.relatedTarget

ondrop is where the move is committed:

var card = event.relatedTarget; var col = event.target.dataset.col; ...; dropEl.appendChild(card);

event.relatedTarget is the draggable element — the card — while event.target remains the dropzone it was dropped on. Rather than rebuilding the whole board from the CARDS array, this snippet re-parents the *actual existing card element* with a plain appendChild. That's a deliberate choice: the card already has interact(el).draggable(...) wired up on it, so moving the real node preserves that wiring — rebuilding it from scratch would mean re-attaching interact.js listeners to a brand-new element every time.

Why the data model still gets updated

Even though the DOM move is what the user sees, data.col = col updates the underlying CARDS array so the count badges (updateCounts()) and any future full re-render stay consistent with what's actually in each column — the DOM and the data are kept in sync deliberately, not left to drift.

Reusing it

This draggable-card-plus-dropzone-column pattern is the backbone of any cross-container drag interaction: kanban boards, playlist-to-playlist track drags, file-to-folder drops. Pair it with an Interact.js Resizable Panel to see the same library's data-x/data-y idiom applied to free positioning and resizing instead.

Step by step

How to Use

  1. 1
    Add the interact.js CDNInclude the interact.min.js UMD build for the global interact function.
  2. 2
    Paste HTML, CSS, and JSA three-column board renders with cards distributed across To Do, In Progress, and Done.
  3. 3
    Drag a cardIts position tracks the pointer via the data-x/data-y plus transform pattern.
  4. 4
    Hover it over another columnondragenter/ondragleave toggle a highlight the instant overlap crosses the configured threshold.
  5. 5
    Release to dropondrop re-parents the actual card element into the new column and updates its data record.
  6. 6
    Watch the count badgesEach column header shows a live count that updates immediately after every drop.

Real-world uses

Common Use Cases

Task and project boards
Build the canonical three-column board where interact.js makes cards draggable and each column a dropzone that highlights while a valid card hovers over it.
Sales and CRM pipelines
Move deals between stages, using overlap: 0.4 so the target column is chosen reliably instead of flickering between adjacent columns.
Content moderation queues
Drag items between review, approved and rejected lanes, with the real dragged element re-parented via appendChild so its state and listeners survive the drop.
Draggable and dropzone teaching
Show how the two interact.js concerns cooperate, where ondragenter and ondragleave toggle a distinct highlight state on the hovered column.
Internal tool prototypes
Scaffold admin screens quickly, since moving between columns needs no framework state and the board works with plain DOM.

Got questions?

Frequently Asked Questions

Inside dropzone listeners (ondragenter, ondragleave, ondrop), event.target is the DROPZONE element -- here, the column's drop area. Inside draggable listeners (start, move, end), event.target is the DRAGGABLE element -- the card. This is the single most common point of confusion switching between the two APIs, and ondrop specifically also gives you event.relatedTarget for the card being dropped.

The card element already has interact(el).draggable(...) attached to it. Rebuilding the DOM from scratch on every drop would mean creating a brand-new element and having to re-run wireCard() on it to reattach that draggable behavior. Moving the actual existing node with appendChild preserves its interact.js wiring for free, since interact.js binds to the DOM node itself, not to a position in a list.

It sets the minimum fraction of the draggable's area that must overlap a dropzone's area before interact.js considers the draggable "over" it and fires ondragenter/triggers a valid drop. Without a reasonable threshold, a card only barely crossing into a neighboring column could flicker between both columns' highlighted states as the pointer moves.

The visual drag offset is a temporary transform applied only during the gesture. A successful drop moves the actual card element into a new parent via appendChild, where it should sit at that column's normal flow position -- if the old transform offset were left in place, the card would appear shifted away from where it visually landed, since the offset was relative to its OLD position in the OLD column.

Give cards a data attribute for their allowed columns, and inside ondrop check dropEl.dataset.col against that before committing the move -- if disallowed, simply do not update data.col or appendChild the card, leaving it to visually snap back (its transform was already reset in the draggable end listener, so it will render at its original DOM position on the next paint).

Keep the CARDS array in component state for the source of truth, but let interact.js manage the live DOM transform during dragging exactly as in vanilla JS -- do not update React/Vue state on every move event. In ondrop, update state (card.col) and let the framework re-render the column lists normally; interact.js's draggable bindings need to be reattached to newly rendered card elements via a ref callback or effect that runs after each render.