Kanban Drag Columns
A mini kanban board where cards drag both within and across columns.
- Category
- Patterns
- Added
- 2026-08-24
- Deps
- 1
- Updated
- 2026-09-01
Todo2
- Wireframe hero
- Write copy
Doing1
- Build nav
Done1
- Set up repo
Props
This entry takes no configurable props.
"use client";
import { useState } from "react";
import { Reorder } from "framer-motion";
type Card = { id: string; label: string };
type Columns = Record<string, Card[]>;
export function KanbanDragColumns({
initialColumns,
}: {
initialColumns: Columns;
}) {
const [columns, setColumns] = useState<Columns>(initialColumns);
const names = Object.keys(columns);
function moveCard(cardId: string, from: string, to: string) {
if (from === to) return;
setColumns((prev) => {
const card = prev[from]?.find((c) => c.id === cardId);
if (!card) return prev;
return {
...prev,
[from]: prev[from].filter((c) => c.id !== cardId),
[to]: [...(prev[to] ?? []), card],
};
});
}
return (
<div className="flex gap-3">
{names.map((name) => (
<div key={name} data-column={name} className="w-40 rounded-xl border border-neutral-200 p-2">
<p className="mb-2 px-1 text-[11px] font-bold uppercase tracking-wide text-neutral-400">{name}</p>
<Reorder.Group
axis="y"
values={columns[name]}
onReorder={(newOrder) => setColumns((prev) => ({ ...prev, [name]: newOrder }))}
className="flex flex-col gap-1.5"
>
{columns[name].map((card) => (
<Reorder.Item
key={card.id}
value={card}
onDragEnd={(e, info) => {
const target = document.elementFromPoint(info.point.x, info.point.y);
const col = target?.closest<HTMLElement>("[data-column]");
if (col?.dataset.column) moveCard(card.id, name, col.dataset.column);
}}
whileDrag={{ scale: 1.04, boxShadow: "0 8px 20px rgba(0,0,0,0.15)" }}
className="cursor-grab rounded-lg border border-neutral-200 bg-white px-2.5 py-2 text-xs font-medium active:cursor-grabbing"
>
{card.label}
</Reorder.Item>
))}
</Reorder.Group>
</div>
))}
</div>
);
}
About this pattern
Framer Motion's Reorder.Group is a sortable-list primitive, not a kanban primitive — it only reorders items within its own values array, so cross-column movement doesn't come for free. This component bolts that on manually: each column carries a data-column attribute, and on every card's drag end it reads the drop point with document.elementFromPoint, walks up with closest('[data-column]') to find which column the cursor actually ended up over, and if that differs from the card's home column, moves the card object between two arrays in one state update. That elementFromPoint-on-drag-end technique is the real takeaway — it's how you bolt cross-container drag onto any single-list sortable primitive, not just Reorder.Group. Reach for this over a full board library like dnd-kit when you want a small, self-contained board with no extra dependency and don't need virtualized columns or collision-detection strategies; reach for a real library once the board needs to be real — hundreds of cards, swimlanes, persistence. State is a plain Record<string, Card[]> keyed by column name, so names have to stay unique; there's no stable column id separate from the display name.
Curator’s note
Reorder.Group only reorders within its own values array — moving a card to a DIFFERENT column needs a manual elementFromPoint check on drag end to detect which column the cursor actually ended up over, then a state move between the two column arrays.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Draggable Sortable List
patternsA drag-handle-driven list that reorders with a smooth layout swap.
Image Comparison Slider
componentsA draggable before/after divider that reveals one image over another.
Draggable Bottom Sheet Modal
patternsA modal sheet you can drag down to dismiss, with a distance + velocity threshold.