saascode

Make empty dnd-kit columns explicit drop targets

Sortable cards are drop targets, but an empty lane has no card to collide with. Register the column itself and handle container IDs explicitly.

academy · aug 21, 2026 · 2 min read

Your Kanban board works until somebody clears a column. After that, cards dragged into the empty lane snap back to where they started.

That exact asymmetry appeared in a live pipeline board. Seed data hid it: every development column contained a card, so there was always a sortable item for collision detection to find.

In dnd-kit, useSortable() composes draggable and droppable behavior for each item. SortableContext describes the item order, but it does not turn an empty container into a physical drop target. With no items, event.over has nothing to reference unless the lane itself is registered.

Register the container

Give every lane a stable ID and attach useDroppable() to the element that should accept the card:

function KanbanColumn({ columnId, cardIds, children }: Props) {
  const { setNodeRef, isOver } = useDroppable({
    id: `column:${columnId}`,
    data: { type: 'column', columnId },
  })

  return (
    <section
      ref={setNodeRef}
      data-over={isOver || undefined}
      className="min-h-32"
    >
      <SortableContext items={cardIds}>
        {children}
      </SortableContext>
    </section>
  )
}

Namespacing container IDs prevents a column ID from colliding with a card ID. The data payload also lets the drag handler distinguish “over a card” from “over a column” without guessing from string membership.

Resolve both collision shapes

When the pointer is over a card, derive its current container and insertion index. When it is over a column, use that column directly and choose the product's intended position—often the end of the list. Handle over === null as a cancelled drop rather than mutating state.

For multiple containers, dnd-kit's legacy sortable guide explicitly shows a droppable zone around each SortableContext so an emptied column remains reachable. Collision strategy still matters: closestCenter or closestCorners is often more forgiving than rectangle intersection for narrow lanes.

Test the state your fixtures avoid

Start with all columns empty. Add one card, move it into another empty column, move it back, and clear the destination again. Repeat with a keyboard sensor, not only a pointer.

Dragging also needs an accessible alternative. WCAG 2.2 requires functionality that uses dragging to be achievable with a single pointer without dragging unless the movement is essential. A “Move to…” menu or equivalent control is often clearer than forcing every user through drag-and-drop.

The snap-back bug was not in the card. It was in the absence of a target. Empty states are real states, and interactive geometry has to exist even when the data array does not.

end