Skip to main content
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

Kanban

Headless board of movable items in columns. Each column binds its items with v-model; items move by pointer drag or by keyboard, and every move writes the new order back.

Edit this page
Report a Bug
Open issues
View on GitHub
Copy Markdown

PreviewRenders elementIntermediateOct 14, 2026

Usage

Bind each column’s item array with v-model and render items from the column’s items slot prop. Style state through data attributes — data-state on items, data-over on lists.

Todo 3
Write the spec
Sketch the layout
Pick a palette
Doing 1
Build the board
Done 1
Kickoff meeting
Press Space or Enter to pick up an item. Use the arrow keys to choose where it goes, Space or Enter to drop it, and Escape to cancel.
Theme
Mode
Palettes
Accessibility
<script setup lang="ts">
  import { Kanban } from '@vuetify/v0'
  import { ref } from 'vue'

  interface Card {
    id: number
    title: string
  }

  const columns = [
    { id: 'todo', label: 'Todo' },
    { id: 'doing', label: 'Doing' },
    { id: 'done', label: 'Done' },
  ]

  const board = ref<Record<string, Card[]>>({
    todo: [
      { id: 1, title: 'Write the spec' },
      { id: 2, title: 'Sketch the layout' },
      { id: 3, title: 'Pick a palette' },
    ],
    doing: [
      { id: 4, title: 'Build the board' },
    ],
    done: [
      { id: 5, title: 'Kickoff meeting' },
    ],
  })
</script>

<template>
  <Kanban.Root class="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3" label="Project board">
    <Kanban.Column
      v-for="column in columns"
      :id="column.id"
      :key="column.id"
      v-slot="{ items }"
      v-model="board[column.id]"
      class="flex flex-col gap-2 rounded-lg border border-divider bg-surface p-2"
      :label="column.label"
    >
      <div class="flex items-center justify-between px-1 text-sm font-semibold">
        {{ column.label }}
        <span class="text-xs font-normal text-on-surface-variant">{{ items.length }}</span>
      </div>

      <Kanban.List class="flex min-h-24 flex-col gap-2 rounded-md p-1 data-[over]:bg-surface-tint data-[target]:bg-surface-tint">
        <Kanban.Item
          v-for="card in items"
          :key="card.id"
          class="cursor-grab touch-none select-none rounded-md border-2 border-divider bg-surface px-3 py-2 data-[drop=after]:border-b-primary data-[drop=before]:border-t-primary text-sm outline-none focus-visible:ring-2 focus-visible:ring-primary data-[state=dragging]:opacity-40 data-[state=grabbed]:border-primary data-[state=grabbed]:ring-2 data-[state=grabbed]:ring-primary"
          :label="card.title"
          :value="card"
        >
          {{ card.title }}
        </Kanban.Item>
      </Kanban.List>
    </Kanban.Column>

    <Kanban.Instructions class="sr-only" />
    <Kanban.LiveRegion class="sr-only" />
  </Kanban.Root>
</template>

Anatomy

vue
<script setup lang="ts">
  import { Kanban } from '@vuetify/v0'
</script>

<template>
  <Kanban.Root>
    <Kanban.Column>
      <Kanban.List>
        <Kanban.Item />
      </Kanban.List>
    </Kanban.Column>

    <Kanban.Instructions />

    <Kanban.LiveRegion />
  </Kanban.Root>
</template>
PartRole
Kanban.RootOwns the createKanban board and the pointer useDragDrop context. Emits move
Kanban.ColumnRegisters a column. v-model is the column’s item array; disabled and accept gate moves
Kanban.ListDrop zone for one column’s items. Must be the direct parent of its Kanban.Item elements
Kanban.ItemA movable item, identified by value
Kanban.InstructionsKeyboard instructions every item references with aria-describedby
Kanban.LiveRegionPolite live region for move announcements

Examples

WIP limit and locked column

accept(value, from, index) vetoes a card coming in from another column. It has to return synchronously. Doing uses it to cap the column at two cards. disabled freezes a column. Shipped is locked, so nothing moves in, out, or within it.

A pointer drop on a refusing column is rejected and announced. ArrowLeft and ArrowRight skip it. Once Doing is full, ArrowRight from Backlog lands on Review. A reorder inside Doing still works. The veto only runs on the way in.

Style the pending keyboard target from data-target on the column and the list, and from data-drop on the neighbouring item. Column policy lives in columns.ts. The Vue file is the board. The same accept option exists on a column ticket in createKanban. Pointer drags come from useDragDrop.

FileRole
columns.tsColumn policy. Doing caps at two cards. Shipped is locked
wip-limit.vueBoard layout. Each column binds its items with v-model
Backlog
Audit routes
Update deps
Write tests
Doing 1 / 2
Fix login bug
Review
Shipped locked
Release 1.2
Press Space or Enter to pick up an item. Use the arrow keys to choose where it goes, Space or Enter to drop it, and Escape to cancel.

Doing takes at most 2 items and Shipped is locked. With the keyboard, pick an item up with Space and press the right arrow: full or locked columns are skipped.

Theme
Mode
Palettes
Accessibility

Observing moves

@move fires once each time a drop commits. The payload is { value, from, to, fromIndex, toIndex }. A pointer drop and a keyboard commit emit the same object. That covers a reorder inside one column and a transfer between columns.

Arrow keys only move the preview. Escape discards it. Neither emits. A listener can treat @move as the moment the board changed. Use it to record history, sync a server, or show a log.

format.ts turns the payload into the line under the board. The log keeps the four newest lines. createKanban splits that moment into transfer:ticket for a cross-column move and move:ticket on a column’s items for a reorder. @move on Kanban.Root is the one event for both.

FileRole
format.tsTurns a KanbanMovePayload into one log line
move-log.vueTwo-column board that records each committed move
Open
Login fails on Safari
Typo in footer
Closed
Broken avatar upload
Press Space or Enter to pick up an item. Use the arrow keys to choose where it goes, Space or Enter to drop it, and Escape to cancel.
  1. Move an item to see the event.
Theme
Mode
Palettes
Accessibility

Recipes

Item identity

The value bound on Kanban.Item is the identity the board tracks. It must equal (===) an entry of the parent column’s v-model array, and every value on the board must be unique. Objects work by reference; strings and numbers work when they are unique. Key the v-for by a stable id.

vue
<template>
  <Kanban.Column v-slot="{ items }" v-model="todo" label="Todo">
    <Kanban.List>
      <Kanban.Item
        v-for="card in items"
        :key="card.id"
        :label="card.title"
        :value="card"
      >
        {{ card.title }}
      </Kanban.Item>
    </Kanban.List>
  </Kanban.Column>
</template>

Changing the board from code

The bound arrays are the source of truth. Push, splice, or replace them and the board reconciles — new values register, removed values unregister, and a reordered array reorders the column. There is no imperative move API on the component; moving an item between two arrays yourself is a move.

ts
todo.value.push({ id: 6, title: 'New card' })
todo.value = todo.value.filter(card => card.id !== 2)

Swimlanes

orientation="vertical" stacks columns vertically and lays items out horizontally. Keyboard arrows follow the layout: Left and Right move within a column, Up and Down move between columns. Lists expose data-orientation for styling.

Keyboard drop preview

While an item is picked up, the arrow keys move a drop target instead of the item, and the board is not touched until Space or Enter commits one move. Style the pending target from data attributes: data-target on the target Kanban.Column and Kanban.List, and data-drop="before" on the item the dropped one would land before — or data-drop="after" on the last item when it would land at the end. An empty target list carries only data-target.

vue
<template>
  <Kanban.List class="data-[target]:bg-surface-tint">
    <Kanban.Item
      v-for="card in items"
      :key="card.id"
      class="data-[drop=after]:border-b-primary data-[drop=before]:border-t-primary"
      :value="card"
    >
      {{ card.title }}
    </Kanban.Item>
  </Kanban.List>
</template>

Disabling

disabled on Kanban.Root freezes the whole board, on Kanban.Column freezes moves into, out of, and within that column, and on Kanban.Item pins one item. Disabled items drop out of the tab order.

Accessibility

Moves never require a pointer. Each item is focusable and keyboard-movable, every move is announced through Kanban.LiveRegion, and Kanban.Instructions tells screen-reader users how to start. Render both, visually hidden — without them moves still work but are silent.

Keyboard

KeyAction
Space / EnterPick up the focused item; when picked up, move it to the drop target
ArrowUp / ArrowDownMove the drop target one slot within its column (between columns for orientation="vertical")
ArrowLeft / ArrowRightMove the drop target to the previous / next column that can take the item, keeping its index when it fits (within its column for orientation="vertical"). Disabled columns and columns whose accept refuses the item are skipped. RTL-aware
Home / EndMove the drop target to the start / end of its column
EscapeDiscard the drop target; nothing moves
TabDiscard the drop target and move focus on

Each target step is announced. Focus follows the item: when a drop re-renders it in another column, the new element takes focus.

ARIA

PartAttributeValue
Rootrolegroup
Rootaria-labellabel prop, or a locale default
Columnrolegroup
Columnaria-labellabel prop
Listrolelist
Itemrolelistitem
Itemaria-roledescriptionLocale default, draggable item
Itemaria-posinset / aria-setsizePosition within the column
Itemaria-describedbyThe Kanban.Instructions element, when mounted
Itemaria-disabledtrue when the item, its column, or the board is disabled
LiveRegionrole / aria-livestatus / polite

Data Attributes

PartAttributeValues
Rootdata-orientationhorizontal | vertical
Root, Column, List, Itemdata-disabledPresent when disabled
Listdata-overPresent while an accepted pointer drag hovers the list
Column, Listdata-targetPresent while a pending keyboard drop would land here
Listdata-orientationAxis the list’s items flow along
Itemdata-stategrabbed (keyboard) | dragging (pointer) | idle
Itemdata-dropbefore | after — the pending keyboard drop lands on this side of the item

FAQ

Discord
Need help? Join our community for support and discussions ↗
Was this page helpful?

© 2016-1970 Vuetify, LLC
Services
Ctrl+/