Kanban Board in Plain Javascript
Building a Dynamic Kanban Board: Deep Dive into Drag-and-Drop Functionality

Making cool stuff
We will be building a simple kanban board in plain javascript , that can shuffle other task, so you can place the task at exact positions.
This is asked a lot of times in machine coding rounds for frontend interviews also.
Link : https://github.com/biohacker0/Kanban-Board
I am not discussing the html and css of the project, that you can see on the github.
We are focusing on the core logic.
Understanding the Setup:We have draggable items (tasks) represented by HTML elements with the class name "task" and dropable areas (swimlanes) represented by HTML elements with the class name "swim-lane". We've attached event listeners to handle drag and drop events for these elements.
Base HTML Structure that is repeted , so we have swim-lane , which is the drop area driv.
And we have div's called task , there are things that we will drag , so these are our tasks.

<div class="swim-lane">
<div class="task" draggable="true">Task 1</div>
<div class="task" draggable="true">Task 2</div>
<!-- More tasks -->
</div>
<!-- More swim-lanes -->
Breakdown of Key Components:
Retrieving Elements:
draggableItems = document.getElementsByClassName("task");- Selects all HTML elements with the class "task," which likely represent individual tasks.
dropableDivs = document.getElementsByClassName("swim-lane");- Selects all HTML elements with the class "swim-lane," which likely represent the different swim lanes or columns.
let draggableItems = document.getElementsByClassName("task"); let dropableDivs = document.getElementsByClassName("swim-lane");
Event Listeners for Tasks:
dragCb(event): When a task is dragged (dragstart event):- Adds the class "is-dragging" to the task, indicating that it's currently being dragged.
dragendCB(event): When dragging stops (dragend event):- Removes the "is-dragging" class, visually signifying the end of dragging.
function dragendCB(event) {
event.target.classList.remove("is-dragging");
}
function dragCb(event) {
event.target.classList.add("is-dragging");
}
for (let task of draggableItems) {
task.addEventListener("dragstart", dragCb);
task.addEventListener("dragend", dragendCB);
}
Drop Event Listener for Swim Lanes:
dragoverCB(event): When a dragged task is hovered over a swim lane (dragover event):Prevents the default browser behavior, which might interfere with custom drag-and-drop logic.
Calls the helper function
insertAboveTaskto find the appropriate insertion point within the swim lane.Retrieves the currently dragged task element using
document.querySelector(".is-dragging").If there's no task below the mouse pointer (meaning the task would be appended to the bottom):
- Simply appends the dragged task to the swim lane using
appendChild.
- Simply appends the dragged task to the swim lane using
Otherwise, inserts the dragged task before the task that's closest below the mouse pointer using
insertBefore.for (let dropableDiv of dropableDivs) { dropableDiv.addEventListener("dragover", function dragoverCB(event) { event.preventDefault(); const bottomTask = insertAboveTask(dropableDiv, event.clientY); const currentTask = document.querySelector(".is-dragging"); if (!bottomTask) dropableDiv.appendChild(currentTask); else dropableDiv.insertBefore(currentTask, bottomTask); }); }
Finding the Insertion Point (
insertAboveTaskfunction):Takes the swim lane element and the mouse Y coordinate as input.
Initializes variables to track the closest task and its offset from the mouse pointer.
Loops through all task elements within the swim lane:
Uses
getBoundingClientRect()to get the task's top position.Calculates the offset between the mouse pointer and the task's top position.
If the offset is negative (i.e., the mouse pointer is above the task) and closer than the current closest offset:
- Updates the closest offset and closest task variables.
Returns the closest task, which will be used to determine the insertion point.

function insertAboveTask(dropableDiv, mouseY) { const currentDropableDivTasks = dropableDiv.getElementsByClassName("task"); let closestTask = null; let closestOffset = Number.NEGATIVE_INFINITY; for (task of currentDropableDivTasks) { const { top } = task.getBoundingClientRect(); const offset = mouseY - top; if (offset < 0 && offset > closestOffset) { closestOffset = offset; closestTask = task; } } return closestTask; }
Core Logic to decide who is closest task to use :
Core Logic Breakdown
Event Listeners:
dragstartanddragendevents are attached to draggable elements (tasks) to track the drag state and add/remove a visual indicator.dragoverevent is attached to dropable elements (swimlanes) to handle the drag-and-drop process.
insertAboveTaskFunction:Takes a
dropableDivandmouseYas input.Iterates through all
taskelements within thedropableDiv.Calculates the offset between the mouse's Y coordinate (
mouseY) and the top of eachtask.Finds the
taskwith the smallest negative offset (closest to the mouse cursor above its current position).If no suitable
taskis found (the mouse is above all tasks), the new task is appended to the end of thedropableDiv.Otherwise, the new task is inserted before the identified
task.
Understanding Offset Calculations
The insertAboveTask function's key logic lies in its offset calculations. Here's a breakdown:
offset = mouseY - top: This calculates the difference between the mouse's Y coordinate and the top edge of each task.offset < 0 && offset > closestOffset: This ensures theoffsetis negative (mouse is above the task) and closer to 0 than any previously encountered offset, indicating it's the closest task above the mouse.Finding the Closest Task: The loop iterates through all tasks, comparing offsets and updating
closestTaskif a closer one is found.Insertion Decision: If no suitable
closestTaskis found (offset === Number.NEGATIVE_INFINITY), the new task is appended to the end. Otherwise, it's inserted before theclosestTask.

Calculations of different scenarios :
Sample dropableDiv with 3 tasks:
Task 1: top = 100px
Task 2: top = 200px
Task 3: top = 300px
Mouse position (mouseY):
Case 1: mouseY = 150px
Case 2: mouseY = 250px
Case 3: mouseY = 350px
Calculations and Results:
Case 1:
Loop iterates through tasks.
Task 1: offset = 50 (positive, ignored)
Task 2: offset = -50 (negative and closest, closestTask updated)
Task 3: offset = -150 (negative but further, ignored)
insertAboveTaskreturnsTask 2(dragged task inserted below).
Case 2:
- Similar logic, but
closestTaskwill beTask 3as offset is closer to 0.
- Similar logic, but
Case 3:
- No negative offsets found,
closestTaskremains null, and the dragged task is appended to the end.
- No negative offsets found,
With this the core logic of kanban is done , yay


