Vitral 0.2
Data

Taskboard

A kanban board of cards in columns and swimlanes, dragged by mouse, touch or keyboard, with work-in-progress limits.

Import

main.ts
import { Taskboard } from '@vitral/vue';

Basic

A flat list of tasks, each naming its column. In progress holds two at most.

Backlog1 cards
To do2 cards
In progress2 of 2 cards
Done1 cards
  • Draft the release notesDocs
  • Audit colour contrastDesignHigh
  • Virtual scrolling in SelectWeb
  • Keyboard grid for DataGridWebHigh
  • Theme editor prototypeDesign
  • Publish 0.1WebHighLocked
Move a card to see the card-move event.
<script setup lang="ts">import { Button, Tag, Taskboard, type TaskboardCardMoveEvent, type TaskboardColumn } from '@vitral/vue';import { ref } from 'vue'; interface Task {    id: number;    title: string;    status: string;    team: string;    priority: 'low' | 'high';    locked?: boolean;} const columns = ref<TaskboardColumn[]>([    { key: 'backlog', title: 'Backlog', color: 'var(--vt-chart-8)' },    { key: 'todo', title: 'To do', color: 'var(--vt-chart-4)' },    { key: 'doing', title: 'In progress', wipLimit: 2, color: 'var(--vt-chart-5)' },    { key: 'done', title: 'Done', color: 'var(--vt-chart-3)' }]); const tasks = ref<Task[]>([    { id: 1, title: 'Draft the release notes', status: 'backlog', team: 'Docs', priority: 'low' },    { id: 2, title: 'Audit colour contrast', status: 'todo', team: 'Design', priority: 'high' },    { id: 3, title: 'Virtual scrolling in Select', status: 'todo', team: 'Web', priority: 'low' },    { id: 4, title: 'Keyboard grid for DataGrid', status: 'doing', team: 'Web', priority: 'high' },    { id: 5, title: 'Theme editor prototype', status: 'doing', team: 'Design', priority: 'low' },    { id: 6, title: 'Publish 0.1', status: 'done', team: 'Web', priority: 'high', locked: true }]); let next = 7;function add(column: string | number, team = 'Web') {    tasks.value = [...tasks.value, { id: next, title: `New task ${next++}`, status: String(column), team, priority: 'low' }];} const log = ref<string[]>([]);function onMove(event: TaskboardCardMoveEvent) {    const task = event.item as Task;    log.value = [`${task.title}: ${event.from.column} → ${event.to.column} (#${event.to.index + 1}, ${event.via})`, ...log.value].slice(0, 3);}</script> <template>    <Taskboard v-model:columns="columns" v-model:items="tasks" column-field="status" style="width: 100%" @card-move="onMove">        <template #card="{ item, locked }">            <div style="display: flex; flex-direction: column; gap: 0.5rem">                <span>{{ (item as Task).title }}</span>                <span style="display: flex; gap: 0.375rem; align-items: center">                    <Tag :value="(item as Task).team" severity="secondary" />                    <Tag v-if="(item as Task).priority === 'high'" value="High" severity="danger" />                    <small v-if="locked" style="color: var(--vt-text-muted-color)">Locked</small>                </span>            </div>        </template>        <template #add-card="{ column }">            <Button label="Add task" icon="plus" variant="text" size="small" severity="secondary" fluid @click="add(column.key)" />        </template>    </Taskboard>    <small style="color: var(--vt-text-muted-color)">{{ log.length ? log.join(' · ') : 'Move a card to see the card-move event.' }}</small></template>

Swimlanes

The same tasks split by team. Page Up and Page Down carry a picked-up card between lanes; a lane collapses from its header.

Backlog1 cards
To do2 cards
In progress2 of 2 cards
Done1 cards
Docs1 tasks
  • Draft the release notes
    No cards
      No cards
        No cards
        Design2 tasks
          No cards
          • Audit colour contrast
          • Theme editor prototype
            No cards
            Web3 tasks
              No cards
              • Virtual scrolling in Select
              • Keyboard grid for DataGrid
              • Publish 0.1
              <script setup lang="ts">import { Taskboard, type TaskboardColumn } from '@vitral/vue';import { ref } from 'vue'; interface Task {    id: number;    title: string;    status: string;    team: string;    priority: 'low' | 'high';    locked?: boolean;} const columns = ref<TaskboardColumn[]>([    { key: 'backlog', title: 'Backlog', color: 'var(--vt-chart-8)' },    { key: 'todo', title: 'To do', color: 'var(--vt-chart-4)' },    { key: 'doing', title: 'In progress', wipLimit: 2, color: 'var(--vt-chart-5)' },    { key: 'done', title: 'Done', color: 'var(--vt-chart-3)' }]); const tasks = ref<Task[]>([    { id: 1, title: 'Draft the release notes', status: 'backlog', team: 'Docs', priority: 'low' },    { id: 2, title: 'Audit colour contrast', status: 'todo', team: 'Design', priority: 'high' },    { id: 3, title: 'Virtual scrolling in Select', status: 'todo', team: 'Web', priority: 'low' },    { id: 4, title: 'Keyboard grid for DataGrid', status: 'doing', team: 'Web', priority: 'high' },    { id: 5, title: 'Theme editor prototype', status: 'doing', team: 'Design', priority: 'low' },    { id: 6, title: 'Publish 0.1', status: 'done', team: 'Web', priority: 'high', locked: true }]);</script> <template>    <Taskboard v-model:columns="columns" v-model:items="tasks" column-field="status" lane-field="team" :reorder-columns="false" style="width: 100%" scroll-height="14rem">        <template #lane-header="{ lane, count }">            <strong>{{ lane.title }}</strong>            <small style="color: var(--vt-text-muted-color)">{{ count }} tasks</small>        </template>    </Taskboard></template>

              Nested columns

              Columns that carry their own cards (v-model:columns). Later starts collapsed but still takes drops; Frozen is locked, and its card is disabled.

              Ideas2 cards
              Next1 cards
              Later2 cards
              Frozen1 cards
              • Dark mode for charts
              • Right-to-left layout
              • Nuxt module
                • IE support
                <script setup lang="ts">import { Taskboard, type TaskboardColumn } from '@vitral/vue';import { ref } from 'vue'; const nested = ref<TaskboardColumn[]>([    { key: 'ideas', title: 'Ideas', items: [{ id: 'a', title: 'Dark mode for charts' }, { id: 'b', title: 'Right-to-left layout' }] },    { key: 'next', title: 'Next', items: [{ id: 'c', title: 'Nuxt module' }] },    { key: 'later', title: 'Later', collapsed: true, items: [{ id: 'd', title: 'Angular adapter' }, { id: 'e', title: 'React adapter' }] },    { key: 'frozen', title: 'Frozen', locked: true, items: [{ id: 'f', title: 'IE support', disabled: true }] }]);</script> <template>    <Taskboard v-model:columns="nested" style="width: 100%">        <template #column-footer="{ count }">            <small style="color: var(--vt-text-muted-color)">{{ count }} in this column</small>        </template>    </Taskboard></template>

                canDrop

                A hook has the last word: here only high-priority tasks may go to Done from anywhere but In progress.

                Backlog1 cards
                To do2 cards
                In progress2 of 2 cards
                Done1 cards
                • Draft the release notes
                • Audit colour contrast
                • Virtual scrolling in Select
                • Keyboard grid for DataGrid
                • Theme editor prototype
                • Publish 0.1
                <script setup lang="ts">import { Taskboard, type TaskboardColumn } from '@vitral/vue';import { ref } from 'vue'; interface Task {    id: number;    title: string;    status: string;    team: string;    priority: 'low' | 'high';    locked?: boolean;} const columns = ref<TaskboardColumn[]>([    { key: 'backlog', title: 'Backlog', color: 'var(--vt-chart-8)' },    { key: 'todo', title: 'To do', color: 'var(--vt-chart-4)' },    { key: 'doing', title: 'In progress', wipLimit: 2, color: 'var(--vt-chart-5)' },    { key: 'done', title: 'Done', color: 'var(--vt-chart-3)' }]); const tasks = ref<Task[]>([    { id: 1, title: 'Draft the release notes', status: 'backlog', team: 'Docs', priority: 'low' },    { id: 2, title: 'Audit colour contrast', status: 'todo', team: 'Design', priority: 'high' },    { id: 3, title: 'Virtual scrolling in Select', status: 'todo', team: 'Web', priority: 'low' },    { id: 4, title: 'Keyboard grid for DataGrid', status: 'doing', team: 'Web', priority: 'high' },    { id: 5, title: 'Theme editor prototype', status: 'doing', team: 'Design', priority: 'low' },    { id: 6, title: 'Publish 0.1', status: 'done', team: 'Web', priority: 'high', locked: true }]); // Only high-priority work may skip straight to Done.const canDrop = ({ item, from, to }: { item: unknown; from: { column: string | number }; to: { column: string | number } }) =>    !(to.column === 'done' && from.column !== 'doing' && (item as Task).priority !== 'high');</script> <template>    <Taskboard v-model:columns="columns" v-model:items="tasks" column-field="status" :can-drop="canDrop" style="width: 100%" /></template>

                API

                Read from packages/vue/src/components/Taskboard/types.ts, so it says what the component actually accepts.

                Props

                NameTypeDescription
                columnsTaskboardColumn[]The columns, in order. With no `items`, each carries its own cards (`v-model:columns`).
                itemsunknown[]The cards as one flat list (`v-model:items`); each names its column in `columnField`.
                columnFieldstringThe field of a flat item that holds its column's key. Defaults to `'column'`.
                laneFieldstringThe field that places a card in a swimlane; set it to split the board into lanes.
                lanesTaskboardLane[]The lanes, in order. Without them, the lanes are the distinct values of `laneField`, in order of appearance.
                dataKeystringThe field that identifies a card. Defaults to `'id'`.
                cardLabelstringThe field holding a card's title, shown by the default card and used in the announcements. Defaults to `'title'`.
                disabledFieldstringThe field that marks a card disabled: not clickable and not movable. Defaults to `'disabled'`.
                lockedFieldstringThe field that marks a card locked: clickable but not movable. Defaults to `'locked'`.
                canDrop(context: TaskboardDropContext) => booleanLast word on a drop. Return false to refuse it.
                dragdropbooleanCards can be dragged (pointer, touch or keyboard). Defaults to true.
                reorderColumnsbooleanColumns can be reordered by their handle. Defaults to true.
                collapsiblebooleanColumns (and lanes) get a button that collapses them. Defaults to true.
                autoScrollbooleanScroll the board and the columns while a card is dragged near their edges. Defaults to true.
                touchDelaynumberMilliseconds a finger rests on a card before it drags, so a swipe still scrolls. Defaults to 250.
                scrollHeightstringThe height of a column's card list; it scrolls past it.
                disabledboolean—

                Plus pt, dt and unstyled from BaseProps, see pass-through and unstyled mode.

                Emits

                EventPayloadDescription
                update:columnsvalue: TaskboardColumn[]The columns after a card or column moved on a nested board, or after a column moved on a flat one.
                update:itemsvalue: unknown[]The flat list after a card moved.
                card-moveevent: TaskboardCardMoveEvent—
                column-moveevent: TaskboardColumnMoveEvent—
                card-clickevent: { item: unknown; column: TaskboardColumn; originalEvent: Event }—
                drop-refusedevent: TaskboardDropRefusedEvent—

                Slots

                NameSlot propsDescription
                default—The parts, written as children: `<Taskboard.Card>`, `<Taskboard.ColumnHeader>`…
                card(props: TaskboardCardSlotProps)A card's content.
                column-header(props: TaskboardColumnSlotProps)A column header's title area.
                column-footer(props: TaskboardColumnSlotProps)Under a column.
                add-card(props: { column: TaskboardColumn; lane?: TaskboardLane })At the end of every cell: an "add card" button, typically.
                lane-header(props: { lane: TaskboardLane; count: number; collapsed: boolean; toggle: () => void })A lane header's title area.
                empty(props: { column: TaskboardColumn; lane?: TaskboardLane })An empty cell.