LEARNING OBJECTIVES โต
- Architect a multi-column Kanban board with decoupled data state and DOM rendering.
- Calculate dynamic card insertion positions based on vertical cursor midpoints (
clientY). - Render animated drop placeholder indicators to preview exact landing positions.
- Handle both intra-column card reordering and cross-column card transitions.
- Persist board modifications to local state storage.
๐ The Mental Model & Story (Intuitive Foundation)
Imagine managing a physical factory floor with three staging zones: Raw Materials, Assembly Line, and Quality Control.
Each workstation has a magnetic whiteboard with numbered task magnets.
When a supervisor picks up a magnet from "Raw Materials":
- The Grab (
dragstart): The supervisor detaches the magnet. A visual placeholder silhouette is left behind to reserve space. - The Evaluation (
dragoverover Assembly Line): The supervisor hovers the magnet over the Assembly Line board. As their hand moves between existing magnets #1 and #2, the board's magnetic sensors detect whether the supervisor is holding the magnet above or below the midpoint of magnet #1. If below the midpoint, existing magnets slide down to create an open gap (the insertion placeholder). - The Lock-In (
drop): The supervisor snaps the magnet into the newly opened gap. - The Ledger Update (
State Sync): The plant manager updates the database so all workers and downstream systems know the task has moved from "Raw Materials" to "Assembly Line at Position 2".
+----------------------------------------------------------------------------------------------------+
| KANBAN MIDPOINT INSERTION ALGORITHM |
+----------------------------------------------------------------------------------------------------+
CARD 1: Top = 100px, Height = 60px --> Midpoint = 100 + (60 / 2) = 130px
==========================================================================
Case A: Mouse Cursor Y = 115px (Above Midpoint 130px)
-----------------------------------------------------
[ INSERTION PLACEHOLDER GAP ] <-- Insert before Card 1
[ Card 1 ]
[ Card 2 ]
Case B: Mouse Cursor Y = 145px (Below Midpoint 130px)
-----------------------------------------------------
[ Card 1 ]
[ INSERTION PLACEHOLDER GAP ] <-- Insert after Card 1 / before Card 2
[ Card 2 ]
Technical Deep Dive & Specifications
The Vertical Midpoint Reordering Algorithm
When dragging over a list of items, how do you determine whether the dropped card should go above or below an existing card?
For each sibling card inside the target column:
- Obtain the card's bounding box using
element.getBoundingClientRect(). - Compute the card's vertical center: $$\text{Midpoint } Y = \text{rect.top} + \frac{\text{rect.height}}{2}$$
- Compare the current cursor
event.clientYwith the midpoint:- If
clientY < Midpoint Y, insert the placeholder before the card. - If
clientY >= Midpoint Y, continue checking the next sibling or insert after the card.
- If
function getDragAfterElement(container, y) {
// Select all draggable cards in this container except the card currently being dragged
const draggableElements = [...container.querySelectorAll('.kanban-card:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2; // Distance from midpoint
// We only care about cards where cursor is ABOVE the midpoint (offset < 0)
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
State-Driven vs DOM-Driven Architecture
In senior enterprise applications, never treat the DOM as your single source of truth. Always synchronize state:
[ User Drops Card ]
|
v
1. Calculate New Column ID & Target Array Index
|
v
2. Update In-Memory JavaScript State Model (boardState.columns)
|
v
3. Persist State (localStorage / REST API / WebSocket)
|
v
4. Re-render UI or commit optimistic DOM mutations
๐ป Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Line 51โ57 (
.drop-placeholder): Defines the high-visibility cyan dashed slot where the dragged card will land if dropped. - Line 115โ125 (
list.addEventListener('dragover', ...)): On every dragover sweep, calculates the nearest sibling viagetDragAfterElementand dynamically positions theplaceholderelement right in the DOM flow. - Line 137โ148 (
function getDragAfterElement(container, y)): Performs the reduction algorithm over sibling bounding boxes to find the element whose vertical midpoint is immediately below the cursor. - Line 129 (
list.insertBefore(draggedCard, placeholder)): Replaces the placeholder with the actual dragged card on drop.
Expected Browser Render Output
+------------------------+ +------------------------+ +------------------------+
| TO DO (2) | | IN PROGRESS (1) | | DONE (1) |
| +--------------------+ | | +--------------------+ | | +--------------------+ |
| | ๐ Audit IAM | | | | ๐ WebSockets | | | | ๐ Publish API Docs | |
| +--------------------+ | | +--------------------+ | | +--------------------+ |
| +--------------------+ | | | | |
| | โก Optimize DB | | | | | |
| +--------------------+ | | | | |
+------------------------+ +------------------------+ +------------------------+๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Persistent Priority Kanban with LocalStorage
Instructions:
Extend the Kanban board architecture with a persistent state model stored in
localStorage.Define a data model:
Whenever a card is dropped (either reordered within the same column or moved to another column), serialize the new board order to
localStorage.setItem('kanban_state', JSON.stringify(state)).On page load, read
localStorageand automatically populate the columns from saved data.
๐ Starter Code Sandbox
โ ๏ธ Common Pitfalls
- Including the Dragged Card in Midpoint Calculations: If you query
.querySelectorAll('.card')without:not(.dragging), the currently dragged card will skew its own midpoint calculations, causing erratic jumping. - Forgetting to Remove the Placeholder on Cancel: If the user hits the Escape key or drops outside the columns,
dropwon't fire. Always clean upplaceholder.remove()insidedragend. - Unchecked Layout Thrashing: Running
getBoundingClientRect()on 500 cards in a massive board on everydragoverframe can degrade FPS. In huge boards, cache card positions or use a virtualized viewport.
๐ก Pro Tips
- Smooth CSS Grid Transitions: Apply
transition: transform 0.2s cubic-bezier(0.2, 0, 0, 1)on sibling cards to create slick, hardware-accelerated Trello-like card slide animations when placeholders appear. - Fractional Ordering for Databases: When persisting card positions to SQL databases (e.g. Postgres), don't re-index all integers (1, 2, 3, 4...). Instead, assign floating-point positions (e.g. position 1.5 between 1.0 and 2.0) to achieve $O(1)$ database updates!
๐ Key Takeaways
- Production Kanban boards rely on vertical midpoint calculation (
box.top + box.height / 2) to determine insertion indexes. - Use dynamic placeholder
<div>elements inserted viainsertBefore()duringdragoverto preview card landing spots. - Always exclude the dragged card from midpoint calculation using
.card:not(.dragging). - Guaranteed placeholder cleanup belongs in
dragend. - Keep data state models synchronized with DOM mutations and persist via
localStorageor backend APIs. - --