beta
Kanban — <mv-kanban>
Kanban board in open lanes divided by hairline rules: pointer drag and drop with a flat floating copy, a hatched reserved slot and cards that slide into place (FLIP), a settle ring on drop, auto-scroll, WIP load bars for limited columns, and full keyboard moves (Space, arrow keys, Escape) announced to screen readers.
| Category | Blocks |
|---|---|
| Type | Web Component (<mv-kanban>) |
| Status | beta |
| Keywords | kanban, board, drag-and-drop, dnd, sortable, tasks, project |
When to use
- A project tool needs a board where tasks move between columns by drag and drop or keyboard
- Columns have work-in-progress limits that should show a load bar and flag overflow
- Many cards must fit on screen with a compact single-line density or tinted column panels
Avoid when
- Items are records to sort and filter rather than tasks moving through stages → use Data table instead
Install
node scripts/add.mjs kanban --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["kanban"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/kanban/kanban.js, components/kanban/kanban.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<mv-kanban id="kb-demo" label="Sprint 42 — Storefront redesign" style="height:640px;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-bg);overflow:hidden">
<style>
#kb-demo .kb-demo-view { display: inline-flex; gap: 2px; padding: 2px; border-radius: var(--mv-radius-md); background: var(--mv-bg-muted); }
#kb-demo .kb-demo-view .mv-button[aria-pressed="true"] { background: var(--mv-surface); color: var(--mv-fg); box-shadow: 0 0 0 1px var(--mv-border); }
</style>
<header class="mv-kanban-head">
<div>
<h2>Sprint 42 · Storefront redesign</h2>
<p>Sep 22 → Oct 3 · drag a card, or focus it, press Space and use the arrow keys</p>
</div>
<div class="mv-kanban-head-end">
<div class="kb-demo-view" role="group" aria-label="Board style">
<button class="mv-button" data-variant="ghost" data-size="sm" type="button" aria-pressed="true" data-kb-view="lanes">Lanes</button>
<button class="mv-button" data-variant="ghost" data-size="sm" type="button" aria-pressed="false" data-kb-view="panels">Panels</button>
<button class="mv-button" data-variant="ghost" data-size="sm" type="button" aria-pressed="false" data-kb-view="compact">Compact</button>
</div>
<div class="mv-avatar-group" data-size="sm" role="group" aria-label="Sprint team">
<span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:200" role="img" aria-label="Chloe Ramirez"><span class="mv-avatar-fallback" aria-hidden="true">CR</span></span>
<span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:280" role="img" aria-label="Ivy Morgan"><span class="mv-avatar-fallback" aria-hidden="true">IM</span></span>
<span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:160" role="img" aria-label="Kwame Boateng"><span class="mv-avatar-fallback" aria-hidden="true">KB</span></span>
<span class="mv-avatar-more" aria-label="2 more members">+2</span>
</div>
<button class="mv-button" data-size="sm" type="button" data-kb-demo-add><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>New task</button>
</div>
</header>
<div class="mv-kanban-board">
<section class="mv-kanban-column" data-column="todo" style="--mv-kanban-accent:var(--mv-fg-subtle)">
<header class="mv-kanban-column-header"><h3 class="mv-kanban-column-title">Backlog</h3><span class="mv-kanban-count"></span></header>
<ul class="mv-kanban-list">
<li class="mv-kanban-card" data-id="NOV-151">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="info" data-size="sm">Design</span><span class="mv-kanban-card-id">NOV-151</span></div>
<p class="mv-kanban-card-title">Mobile home screen mockups</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="2" role="img" aria-label="Medium priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Oct 2</span><span aria-label="3 comments"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>3</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:280" role="img" aria-label="Assigned to Ivy Morgan"><span class="mv-avatar-fallback" aria-hidden="true">IM</span></span></div>
</li>
<li class="mv-kanban-card" data-id="NOV-149">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="secondary" data-size="sm">Content</span><span class="mv-kanban-card-id">NOV-149</span></div>
<p class="mv-kanban-card-title">Write the billing FAQ</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="1" role="img" aria-label="Low priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Oct 4</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:240" role="img" aria-label="Assigned to Tom Garcia"><span class="mv-avatar-fallback" aria-hidden="true">TG</span></span></div>
</li>
<li class="mv-kanban-card" data-id="NOV-147">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="secondary" data-size="sm">Dev</span><span class="mv-kanban-card-id">NOV-147</span></div>
<p class="mv-kanban-card-title">Wire up CSV export for orders</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="2" role="img" aria-label="Medium priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Oct 6</span><span aria-label="1 comment"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>1</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:160" role="img" aria-label="Assigned to Kwame Boateng"><span class="mv-avatar-fallback" aria-hidden="true">KB</span></span></div>
</li>
<li class="mv-kanban-card" data-id="NOV-146">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="warning" data-size="sm">Accessibility</span><span class="mv-kanban-card-id">NOV-146</span></div>
<p class="mv-kanban-card-title">Dark mode contrast audit</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="3" role="img" aria-label="High priority"><i></i><i></i><i></i></span><span data-tone="danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Sep 30</span><span aria-label="5 comments"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>5</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:320" role="img" aria-label="Assigned to Sofia Nguyen"><span class="mv-avatar-fallback" aria-hidden="true">SN</span></span></div>
</li>
</ul>
</section>
<section class="mv-kanban-column" data-column="doing" data-limit="3" style="--mv-kanban-accent:var(--mv-info)">
<header class="mv-kanban-column-header"><h3 class="mv-kanban-column-title">In progress</h3><span class="mv-kanban-count"></span></header>
<ul class="mv-kanban-list">
<li class="mv-kanban-card" data-id="NOV-140">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="secondary" data-size="sm">Dev</span><span class="mv-kanban-card-id">NOV-140</span></div>
<p class="mv-kanban-card-title">Interest-free installment payments</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="3" role="img" aria-label="High priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Sep 27</span><span aria-label="8 comments"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>8</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:200" role="img" aria-label="Assigned to Chloe Ramirez"><span class="mv-avatar-fallback" aria-hidden="true">CR</span></span></div>
</li>
<li class="mv-kanban-card" data-id="NOV-138">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="info" data-size="sm">Design</span><span class="mv-kanban-card-id">NOV-138</span></div>
<p class="mv-kanban-card-title">Redesign product cards</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="2" role="img" aria-label="Medium priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Oct 1</span><span aria-label="2 comments"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>2</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:280" role="img" aria-label="Assigned to Ivy Morgan"><span class="mv-avatar-fallback" aria-hidden="true">IM</span></span></div>
</li>
<li class="mv-kanban-card" data-id="NOV-135">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="secondary" data-size="sm">Dev</span><span class="mv-kanban-card-id">NOV-135</span></div>
<p class="mv-kanban-card-title">Migrate images to AVIF</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="1" role="img" aria-label="Low priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Oct 3</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:160" role="img" aria-label="Assigned to Kwame Boateng"><span class="mv-avatar-fallback" aria-hidden="true">KB</span></span></div>
</li>
</ul>
</section>
<section class="mv-kanban-column" data-column="review" data-limit="2" style="--mv-kanban-accent:var(--mv-warning)">
<header class="mv-kanban-column-header"><h3 class="mv-kanban-column-title">In review</h3><span class="mv-kanban-count"></span></header>
<ul class="mv-kanban-list">
<li class="mv-kanban-card" data-id="NOV-131">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="secondary" data-size="sm">Dev</span><span class="mv-kanban-card-id">NOV-131</span></div>
<p class="mv-kanban-card-title">Guest checkout flow</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="2" role="img" aria-label="Medium priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Sep 26</span><span aria-label="4 comments"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>4</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:240" role="img" aria-label="Assigned to Tom Garcia"><span class="mv-avatar-fallback" aria-hidden="true">TG</span></span></div>
</li>
<li class="mv-kanban-card" data-id="NOV-129">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="secondary" data-size="sm">Content</span><span class="mv-kanban-card-id">NOV-129</span></div>
<p class="mv-kanban-card-title">German and Italian translations</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="1" role="img" aria-label="Low priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Sep 28</span><span aria-label="1 comment"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>1</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:320" role="img" aria-label="Assigned to Sofia Nguyen"><span class="mv-avatar-fallback" aria-hidden="true">SN</span></span></div>
</li>
</ul>
</section>
<section class="mv-kanban-column" data-column="done" style="--mv-kanban-accent:var(--mv-success)">
<header class="mv-kanban-column-header"><h3 class="mv-kanban-column-title">Done</h3><span class="mv-kanban-count"></span></header>
<ul class="mv-kanban-list">
<li class="mv-kanban-card" data-id="NOV-124">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="info" data-size="sm">Design</span><span class="mv-kanban-card-id">NOV-124</span></div>
<p class="mv-kanban-card-title">New color palette</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="2" role="img" aria-label="Medium priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Sep 19</span><span aria-label="6 comments"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>6</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:280" role="img" aria-label="Assigned to Ivy Morgan"><span class="mv-avatar-fallback" aria-hidden="true">IM</span></span></div>
</li>
<li class="mv-kanban-card" data-id="NOV-120">
<div class="mv-kanban-card-top"><span class="mv-badge" data-variant="secondary" data-size="sm">Dev</span><span class="mv-kanban-card-id">NOV-120</span></div>
<p class="mv-kanban-card-title">Instant catalog search</p>
<div class="mv-kanban-card-meta"><span class="mv-kanban-priority" data-level="3" role="img" aria-label="High priority"><i></i><i></i><i></i></span><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Sep 17</span><span aria-label="9 comments"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>9</span><span class="mv-avatar" data-variant="tinted" style="--mv-avatar-hue:200" role="img" aria-label="Assigned to Chloe Ramirez"><span class="mv-avatar-fallback" aria-hidden="true">CR</span></span></div>
</li>
</ul>
</section>
</div>
</mv-kanban>
<script type="module">
const board = document.getElementById("kb-demo");
let n = 152;
board?.querySelectorAll("[data-kb-view]").forEach((btn, _, all) => btn.addEventListener("click", () => {
const v = btn.dataset.kbView;
all.forEach((b) => b.setAttribute("aria-pressed", String(b === btn)));
if (v === "panels") board.dataset.variant = "panels"; else delete board.dataset.variant;
if (v === "compact") board.dataset.density = "compact"; else delete board.dataset.density;
}));
board?.querySelector("[data-kb-demo-add]")?.addEventListener("click", () => {
const list = board.querySelector('[data-column="todo"] .mv-kanban-list');
const card = document.createElement("li");
card.className = "mv-kanban-card";
card.dataset.id = `NOV-${n}`;
const top = document.createElement("div");
top.className = "mv-kanban-card-top";
const tag = document.createElement("span");
tag.className = "mv-badge";
tag.dataset.variant = "secondary";
tag.dataset.size = "sm";
tag.textContent = "Triage";
const id = document.createElement("span");
id.className = "mv-kanban-card-id";
id.textContent = `NOV-${n++}`;
top.append(tag, id);
const title = document.createElement("p");
title.className = "mv-kanban-card-title";
title.textContent = "Untitled task";
card.append(top, title);
list.prepend(card);
requestAnimationFrame(() => card.focus());
});
</script>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
label | string | Board | Accessible name of the region. |
data-variant | panels | On <mv-kanban>: tinted column panels with an accent top rule instead of open lanes (default). | |
data-density | compact | On <mv-kanban>: single-line cards (id, title, assignee); badges and other meta are hidden. | |
data-dragging | boolean | Set during a pointer drag. |
Events
| Name | Description |
|---|---|
mv-change | Card moved (pointer or keyboard). detail: { id, card, from, to, index } (from/to = data-column). |
Content structure
| Name | Description |
|---|---|
.mv-kanban-board | Horizontally scrolling container (snaps in narrow containers). |
section.mv-kanban-column[data-column] | Column; data-limit="3" shows 3/3, draws a WIP load bar under the header (--mv-kanban-load) and sets data-full / data-over; --mv-kanban-accent colors its status ring. |
.mv-kanban-column-header > .mv-kanban-column-title + .mv-kanban-count | Title (names the list) and automatic counter; optional .mv-kanban-column-actions. |
ul.mv-kanban-list > li.mv-kanban-card[data-id] | Focusable cards (cards added later are picked up too). Inner links/buttons don’t start a drag; data-kanban-no-drag excludes other areas. |
CSS classes
| Name | Description |
|---|---|
mv-kanban-head / -head-end | Optional board header, ruled off from the lanes. |
mv-kanban-card-top / -card-id / -card-title / -card-meta | Card parts; the mono id leads the top row, the meta row sits under a dashed rule. |
mv-kanban-priority[data-level=1|2|3] / mv-kanban-progress | Stepped-bar priority gauge, thin progress bar (--value). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-kanban-column-width | 17.5rem | Column width. |
--mv-kanban-accent | Column color: status ring, WIP bar, drop-target tint, panel top rule. | |
--mv-kanban-load | Set by the element on limited columns: count / limit (0–1). |
Accessibility
Named region; each list is named by its column title; focusable cards with aria-roledescription “draggable card” and instructions (aria-describedby). Keyboard: arrow keys to browse; Space/Enter picks up (dashed outline + “Moving” tag), arrow keys move (↑↓ within the column, ←→ across columns), Space/Enter drops, Escape cancels; every step is announced (aria-live assertive) with column and position; focus follows the card. Counters carry an aria-label (“2 of 3 cards allowed”). Reduced motion: no FLIP, no drop glide, no settle ring.