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.

CategoryBlocks
TypeWeb Component (<mv-kanban>)
Statusbeta
Keywordskanban, board, drag-and-drop, dnd, sortable, tasks, project

When to use

Avoid when

Install

node scripts/add.mjs kanban --out ./src/marvelous

AI 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

NameTypeDefaultDescription
labelstringBoardAccessible name of the region.
data-variantpanelsOn <mv-kanban>: tinted column panels with an accent top rule instead of open lanes (default).
data-densitycompactOn <mv-kanban>: single-line cards (id, title, assignee); badges and other meta are hidden.
data-draggingbooleanSet during a pointer drag.

Events

NameDescription
mv-changeCard moved (pointer or keyboard). detail: { id, card, from, to, index } (from/to = data-column).

Content structure

NameDescription
.mv-kanban-boardHorizontally 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-countTitle (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

NameDescription
mv-kanban-head / -head-endOptional board header, ruled off from the lanes.
mv-kanban-card-top / -card-id / -card-title / -card-metaCard 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-progressStepped-bar priority gauge, thin progress bar (--value).

CSS variables

NameDefaultDescription
--mv-kanban-column-width17.5remColumn width.
--mv-kanban-accentColumn color: status ring, WIP bar, drop-target tint, panel top rule.
--mv-kanban-loadSet 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.