Skip to content

Kanban ​

WxKanban is a board of columns you drag cards between: tasks by status, orders by stage, pages on their way to being published.

Drag a card or a column heading, or focus one and press space.
Backlog3
Rewrite the pricing page
ContentMaria
Collect the photos for the gallery
MediaOleh
Decide on the new domain
AdminKate
In progress1/2
Migrate the blog posts
ContentOleh
On review1
Checkout on a phone
QAMaria

Usage ​

The board takes columns that carry their own cards. What a card looks like is yours — the card slot gets the card, its column and its index.

vue
<script setup>
const columns = ref([
  { id: 'todo', title: 'To do', items: [{ id: 1, title: 'Rewrite the pricing page' }] },
  { id: 'doing', title: 'In progress', tone: 'primary', limit: 3, items: [] },
  { id: 'done', title: 'Done', tone: 'success', items: [] },
])
</script>

<template>
  <wx-kanban :columns="columns" aria-label="Website tasks" @move="save">
    <template #card="{ card }">
      <wx-entity-card :title="card.title" variant="card" bordered size="sm" />
    </template>
  </wx-kanban>
</template>

A card needs an id and nothing else; everything else on it is yours to read in the slot.

Saving a move ​

The board moves the card between the arrays it was given — that is what makes it land where it was dropped — and then says what happened:

ts
function save(move) {
  // { card, from: { column, index }, to: { column, index }, via: 'pointer' | 'keyboard' }
  axios.patch(`/tasks/${move.card.id}`, { status: move.to.column, position: move.to.index })
}

to.index is the position within its column, counted from zero, which is what a Laravel ->update(['position' => ...]) wants. If the request fails, move the card back yourself — the board holds no state of its own to roll back.

Work-in-progress limits ​

limit on a column is the point of a board rather than a decoration: over it the count turns red and the column stops accepting cards, whether they are dragged or moved with the keyboard. Reordering inside a full column still works — it does not make the column any fuller.

vue
<template>
  <wx-kanban :columns="[{ id: 'doing', title: 'In progress', limit: 3, items }]" />
</template>

disabled on a column freezes it both ways; on the board it freezes everything.

What a column can do ​

The column-actions slot is the end of a heading: a plus, a menu, a filter — whatever this column can be told to do. It sits outside everything the board drags, so pressing a button there never starts a move.

vue
<template>
  <wx-kanban :columns="columns" collapsible reorder-columns column-addable @add-column="create">
    <template #column-actions="{ column }">
      <wx-action type="add" size="sm" title="Add a task" @click="add(column)" />
      <wx-dropdown align="end">
        <template #trigger><wx-action type="more" size="sm" title="Column menu" /></template>
        <wx-dropdown-item icon="trash" tone="danger" @click="clear(column)"
          >Empty it</wx-dropdown-item
        >
      </wx-dropdown>
    </template>
  </wx-kanban>
</template>

collapsible folds a column down to a strip with its name read the long way, which is how a board with a dozen statuses stays legible. Bind v-model:collapsed — an array of column ids — to remember which ones are folded between visits. A folded column holds no cards that can be reached, so it takes none: dragging to it is refused and a card moved with the keyboard passes it by.

vue
<template>
  <wx-kanban v-model:collapsed="folded" :columns="columns" collapsible />
</template>

column-addable puts a column-shaped button after the last column and emits add-column; the default slot replaces that button when the application wants its own.

Reordering the columns ​

reorder-columns lets a column be dragged by its heading — by the heading only, so a card is still picked up by the card. The new order is written into the columns array you passed and reported as { column, from, to, via }.

Keyboard, with a heading focused: Space picks the column up, ← → move it, Space drops it, Esc puts it back.

The keyboard ​

SortableJS is a pointer library and has nothing to say to a keyboard, so the board carries its own. Tab to a card, then:

KeyWhat happens
Space / EnterPick the card up, or drop it
↑ ↓Move it up or down its column
← →Move it to the previous or next column that will take it
EscPut it back where it came from

Every move is read out through a live region, and the focus follows the card, so a board is usable without ever touching a mouse. Keys pressed on a button or a link inside a card are left alone.

On a phone ​

The columns scroll sideways and the swipe snaps to one at a time below 560px — measured on the board, not on the window, so a board in a narrow panel behaves the same way. A drag starts after a short press rather than immediately, which is what tells it apart from a scroll.

The board is as tall as it is given. Put a height on it and each column scrolls its own cards:

vue
<template>
  <wx-kanban class="board" :columns="columns" />
</template>

<style>
.board {
  height: calc(100vh - 200px);
}
</style>

Props ​

PropTypeDefaultDescription
columnsKanbanColumn[]—Required; each carries its own items
collapsedKanbanId[][]Folded columns; use v-model:collapsed
groupstring—Boards sharing a name exchange cards
size'sm' | 'md''md'Padding and gaps
columnWidthnumber | string288A number means pixels
disabledbooleanfalseNothing moves
handlestring—CSS selector of the part that drags
addablebooleanfalseAdds a button under each column
addLabelstring'Add a card'Its label
collapsiblebooleanfalseA column can be folded to a strip
reorderColumnsbooleanfalseColumns drag by their heading
columnAddablebooleanfalseA button after the last column
addColumnLabelstring'Add a column'Its label
emptyTextstring'Nothing here yet'Shown in a column with no cards
ariaLabelstring—Accessible name for the board

Column: { id, title?, items, limit?, tone?, disabled? }, where tone is default | primary | success | warning | danger | info and colours the rule above the column.

Events: move — a card changed position or column; column-move — a column did; add — the button under a column was pressed; add-column — the one after the last column was.

Slots: card ({ card, column, index }); column-header ({ column, count, overLimit }); column-actions ({ column, collapsed }); column-footer ({ column }); empty ({ column }); default — after the last column, in place of the add-a-column button.

Released under the MIT License.