Skip to content

KanbanBoard ​

A lightweight kanban board component with no external drag-and-drop library dependencies. Bold-bordered columns and sharp-shadow cards embody the tension of Neo-Brutalism.

Demo ​

Preview

待办

2

设计首页 Banner

确定配色与排版方案

设计

编写组件文档

文档

进行中

1

开发 Kanban 组件

支持拖拽排序

开发P0

已完成

2

Accordion 组件

完成

Timeline 组件

完成

列拖拽排序与自定义添加按钮

拖拽列标题可重排顺序;待办列使用自定义「新建任务」按钮。

Backlog

1

调研需求

Progress

1

搭建脚手架

Review

1

代码评审

Installation ​

pnpm dlx brutx-vue@latest add kanban

Usage ​

vue
<script setup>
import { KanbanBoard } from 'brutx-ui-vue'
import type { KanbanColumn } from 'brutx-ui-vue'
import { ref } from 'vue'

const columns = ref<KanbanColumn[]>([
    {
        id: 'todo',
        title: 'Todo',
        color: '#FFE66D',
        cards: [
            { id: 'c1', title: 'Requirement analysis', tags: ['Dev'] },
        ],
    },
    {
        id: 'done',
        title: 'Done',
        cards: [],
    },
])
</script>

<template>
    <KanbanBoard
        v-model="columns"
        @card-move="(id, from, to) => console.log(id, from, to)"
    />
</template>

Column Reordering ​

Drag column headers to reorder columns. The ordering is synced via v-model and triggers the column-move event.

vue
<script setup>
import { ref } from 'vue'
import { KanbanBoard } from 'brutx-ui-vue'

const columns = ref([
    { id: 'todo', title: 'Todo', cards: [] },
    { id: 'done', title: 'Done', cards: [] },
])

function handleColumnMove(columnId, fromIndex, toIndex) {
    console.log('Column', columnId, 'moved from', fromIndex, 'to', toIndex)
}
</script>

<template>
    <KanbanBoard v-model="columns" @column-move="handleColumnMove" />
</template>

Custom Add Card Entry ​

Each column renders an outline style "Add card" button at the bottom by default, which triggers the add-card event on click. Use the #add-{columnId} slot to replace it with a custom entry.

vue
<script setup>
import { ref } from 'vue'
import { KanbanBoard, Button } from 'brutx-ui-vue'

const columns = ref([
    { id: 'todo', title: 'Todo', cards: [] },
])

function handleAddCard(columnId) {
    columns.value[0].cards.push({ id: Date.now().toString(), title: 'New card' })
}
</script>

<template>
    <KanbanBoard v-model="columns" @add-card="handleAddCard">
        <template #add-todo="{ columnId }">
            <Button variant="primary" size="sm" class="w-full" @click="handleAddCard(columnId)">
                New Task
            </Button>
        </template>
    </KanbanBoard>
</template>

Data Types ​

ts
interface KanbanCard {
    id: string
    title: string
    description?: string
    tags?: string[]
    color?: string
}

interface KanbanColumn {
    id: string
    title: string
    color?: string
    cards: KanbanCard[]
}

Programmatic Control ​

vue
<script setup>
import { ref } from 'vue'
import { KanbanBoard } from 'brutx-ui-vue'

const kanbanRef = ref(null)
const columns = ref([
    { id: 'todo', title: 'Todo', cards: [{ id: 'card-1', title: 'Task 1' }] },
    { id: 'done', title: 'Done', cards: [] },
])

function moveCardRight() {
    kanbanRef.value?.moveCard('card-1', 'todo', 1)
}
</script>

<template>
    <KanbanBoard ref="kanbanRef" v-model="columns" />
    <button @click="moveCardRight">Move Card Right</button>
</template>

API Reference ​

KanbanBoard — Props
NameTypeDefaultDescription
modelValue
Required
KanbanColumn[]
—
The board columns and cards. Bind with v-model to receive updated data after moves, and keep column and card IDs unique.
Details

Declared type

KanbanColumn[]

Referenced types

KanbanColumn
Type definition — KanbanColumn
export interface KanbanColumn {
    id: string
    title: string
    color?: string
    cards: KanbanCard[]
}

Default value details

Not declared
—

Source

KanbanBoard.vue:16

packages/ui/src/components/kanban/KanbanBoard.vue

class
string | undefined
—
Custom CSS classes merged onto the board container that arranges columns horizontally.
Details

Declared type

string | undefined

Default value details

Not declared
—

Source

KanbanBoard.vue:17

packages/ui/src/components/kanban/KanbanBoard.vue

KanbanBoard — Events
NameSignature or typeDescription
@update:modelValue
[columns: KanbanColumn[]]
Emits a new column array after a successful card move or column reorder, before the corresponding move event.
Details

Declared type

[columns: KanbanColumn[]]

Referenced types

KanbanColumn
Type definition — KanbanColumn
export interface KanbanColumn {
    id: string
    title: string
    color?: string
    cards: KanbanCard[]
}

Source

KanbanBoard.vue

packages/ui/src/components/kanban/KanbanBoard.vue

@card-move
[cardId: string, fromColumn: string, toColumn: string]
Emits the card ID, source column ID, and target column ID after a successful mouse, keyboard, or instance-method move. Both column IDs are equal for reordering within one column.
Details

Declared type

[cardId: string, fromColumn: string, toColumn: string]

Source

KanbanBoard.vue

packages/ui/src/components/kanban/KanbanBoard.vue

@card-click
[card: KanbanCard, columnId: string]
Emits the card and column ID on a click or Enter while focused. It is suppressed during card dragging; Space grabs or releases the card.
Details

Declared type

[card: KanbanCard, columnId: string]

Referenced types

KanbanCard
Type definition — KanbanCard
export interface KanbanCard {
    id: string
    title: string
    description?: string
    tags?: string[]
    color?: string
}

Source

KanbanBoard.vue

packages/ui/src/components/kanban/KanbanBoard.vue

@column-move
[columnId: string, fromIndex: number, toIndex: number]
Emits the column ID and old/new zero-based indices after a successful header drag reorder or moveColumn call.
Details

Declared type

[columnId: string, fromIndex: number, toIndex: number]

Source

KanbanBoard.vue

packages/ui/src/components/kanban/KanbanBoard.vue

@add-card
[columnId: string]
Emits the column ID when the built-in add button is clicked or instance addCard is called. The consumer creates the card and updates the data.
Details

Declared type

[columnId: string]

Source

KanbanBoard.vue

packages/ui/src/components/kanban/KanbanBoard.vue

KanbanBoard — Slots
NameSignature or typeDescription
#add-{columnId}
{ columnId: string }
Replaces the add-card entry at the bottom of the named column and receives its ID. By default, an outline, sm Button emits add-card when clicked.
Details

Declared type

{ columnId: string }

Source

KanbanBoard.vue:360

packages/ui/src/components/kanban/KanbanBoard.vue

KanbanBoard — Exposes
NameSignature or typeDescription
moveCard
(cardId: string, columnId: string, direction: number, mode?: "in-column" | "adjacent") => void
Moves a card by direction and handles focus and announcements. adjacent moves between columns; in-column reorders within the current column. A direction of 1 means right or down; other values mean left or up. The mode defaults to adjacent.
Details

Declared type

(cardId: string, columnId: string, direction: number, mode?: "in-column" | "adjacent") => void

Source

KanbanBoard.vue:267

packages/ui/src/components/kanban/KanbanBoard.vue

moveColumn
(fromId: string, toId: string) => void
Moves the fromId column to the current index of toId, shifting intervening columns. A successful move emits data-update and column-move events.
Details

Declared type

(fromId: string, toId: string) => void

Source

KanbanBoard.vue:270

packages/ui/src/components/kanban/KanbanBoard.vue

addCard
(columnId: string) => void
Emits an add-card request for the given column. The method itself does not create a card.
Details

Declared type

(columnId: string) => void

Source

KanbanBoard.vue:273

packages/ui/src/components/kanban/KanbanBoard.vue

getColumn
(columnId: string) => KanbanColumn | undefined
Returns the column object from the current modelValue by ID, or undefined when absent. It preserves the original object reference.
Details

Declared type

(columnId: string) => KanbanColumn | undefined

Referenced types

KanbanColumn
Type definition — KanbanColumn
export interface KanbanColumn {
    id: string
    title: string
    color?: string
    cards: KanbanCard[]
}

Source

KanbanBoard.vue:274

packages/ui/src/components/kanban/KanbanBoard.vue

getAllColumns
() => KanbanColumn[]
Returns the current modelValue column-array reference. To edit it, construct new data and update the bound value.
Details

Declared type

() => KanbanColumn[]

Referenced types

KanbanColumn
Type definition — KanbanColumn
export interface KanbanColumn {
    id: string
    title: string
    color?: string
    cards: KanbanCard[]
}

Source

KanbanBoard.vue:275

packages/ui/src/components/kanban/KanbanBoard.vue

Accessibility ​

  • Cards support keyboard navigation and can be focused via the Tab key
  • Pressing Enter after focusing triggers card-click; Space grabs or releases the card
  • Empty columns display a localized prompt text to guide users to drag and drop cards
  • Keyboard drag: Focus a card and press Space to grab, use arrow keys to move, press Space again to drop, Escape to cancel
    • ↑/↓: Move card up/down within the current column
    • ←/→: Move card to an adjacent column
  • Grabbed cards have aria-grabbed="true" attribute added
  • Move results are automatically announced via aria-live="polite" region

Keyboard Drag Example ​

vue
<script setup>
import { ref } from 'vue'
import { KanbanBoard } from 'brutx-ui-vue'

const columns = ref([
    { id: 'todo', title: 'Todo', cards: [
        { id: 'c1', title: 'Task 1' },
        { id: 'c2', title: 'Task 2' },
    ]},
    { id: 'done', title: 'Done', cards: [] },
])
</script>

<template>
    <!-- Users can:
         1. Tab to focus a card
         2. Press Space to grab the card
         3. Use arrow keys to move the card
         4. Press Space to drop the card
         5. Press Escape to cancel
    -->
    <KanbanBoard v-model="columns" />
</template>

FAQ ​

Q: Why isn't the data updated after dragging a card?

A: KanbanBoard uses v-model for two-way data binding. Make sure the columns passed in are reactive data (using ref or reactive). The component automatically updates the data via the update:modelValue event after a card move or column reorder. If you use a plain variable, the data will not sync.

Q: Does dragging work on mobile devices?

A: The component is built on the native HTML5 Drag and Drop API, which may have limited support on mobile browsers. If you need to use it on mobile, test compatibility on the target device or consider providing an alternative interaction method (e.g., button-based actions) for mobile.

Q: How can I limit the number of cards in a column?

A: The component does not provide a built-in card count limit. You can implement this by listening to the card-move event, checking the card count of the target column in the event handler, and moving the card back to the original column by modifying the data if the limit is exceeded.

Brute force builds.