Skip to content

Kanban Drag Columns

A mini kanban board where cards drag both within and across columns.

Freeminimaleditorial
Category
Patterns
Added
2026-08-24
Deps
1
Updated
2026-09-01
Preview

Todo2

  • Wireframe hero
  • Write copy

Doing1

  • Build nav

Done1

  • Set up repo

Props

This entry takes no configurable props.

Requires framer-motion
"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>
  );
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

A drag-handle-driven list that reorders with a smooth layout swap.

dragsortablelist

A draggable before/after divider that reveals one image over another.

imageslidercompare

A modal sheet you can drag down to dismiss, with a distance + velocity threshold.

modalsheetdrag