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>)
Statusstable
Keywordskanban, 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/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/focus.js, core/i18n.js, core/motion.js, core/position.js, components/kanban/kanban.js, components/kanban/kanban.css.

Usage

Canonical markup, to start from 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); }
    #kb-demo .kb-demo-view .mv-button[aria-pressed="true"]:not(:focus-visible) { box-shadow: 0 0 0 1px var(--mv-border); }
    @media (forced-colors: active) { #kb-demo .kb-demo-view .mv-button[aria-pressed="true"] { forced-color-adjust: none; background: Highlight; color: HighlightText; } }
  </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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due" 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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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><button type="button" class="mv-kanban-move" aria-label="Move to another column"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></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 class="mv-kanban-card-due"><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, board.querySelector(".mv-kanban-move").cloneNode(true));
    const title = document.createElement("p");
    title.className = "mv-kanban-card-title";
    title.textContent = "Untitled task";
    card.append(top, title);
    list.prepend(card);
    board.refresh(); // prepares the new card right away, so it can take focus now
    card.focus();
  });
</script>

API

Attributes

NameTypeDefaultDescription
labelstringBoardAccessible name of the region.
openablebooleanCards open: Enter and double-click emit mv-open (Space still picks a card up, and Enter drops a lifted one). The hidden instructions mention Enter.
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, due date, assignee, move button); badges and other meta are hidden.
data-draggingbooleanSet during a pointer drag.

Properties

NameTypeDescription
stringsPartial<Record<string, string | { one, other }>>Overrides for the default text (en-US): help (hidden keyboard instructions), helpOpenable (the same with openable), card (aria-roledescription of each card), count (counter label, {n}, plural forms as { one, other }), countLimit (counter label with data-limit, {n} and {limit}), position ("Column {column}, position {index} of {total}."), announceLifted, announceDropped, announceCanceled, announceMoved ({title} of the card and {position}), moveMenu (name of the move menu, {title}), empty (empty column, drawn by the CSS), moving (tag of a card picked up with the keyboard, drawn by the CSS). App-wide translations: setStrings() from core/i18n.js, under the key "kanban". Can be set before the element is defined.

Methods

NameDescription
refresh()Prepares cards and columns inserted since the last pass (focusable, labeled, counted). Runs by itself after any insertion; call it right after inserting a card you want to focus at once.
moveCard(card, column, index?)Moves a card (element or data-id) to the column whose data-column is column, at index (default: the end). Animated, announced and reported with mv-change. Returns false when nothing matches.

Events

NameDescription
mv-changeCard moved (pointer, keyboard, move menu or moveCard()). detail: { id, card, from, to, index } (from/to = data-column).
mv-openWith openable: Enter or double-click on a card (not on its links or buttons). detail: { id, card }.

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, lists and columns added later are picked up too, see refresh()). 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-card-dueDue date span in the meta row (add data-tone="danger" when late). Stays visible in compact density, with the assignee.
mv-kanban-moveIcon button in .mv-kanban-card-top (give it an aria-label): opens a menu of the other columns and moves the card there. The touch path, also handy with a mouse; kept in compact density.
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; with openable, Enter opens the card (mv-open); Space (and Enter without openable) 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. Forced colors: system-color focus outline, column rules and priority bars. Known limits: the “Moving” tag is CSS generated content, English only (override with .mv-kanban-card[data-lifted]::after), while the announcements and labels follow the strings property; on a touch screen a swipe scrolls the board (cards allow panning), so drag and drop needs a mouse or a pen; touch users move cards with the .mv-kanban-move button, a menu (role="menu", arrow keys, Escape returns focus to the button) listing the other columns.