Kanban
A drag-and-drop kanban board driven by a `columns` data array. Cards can be dragged between and within columns with the pointer, or moved entirely from the keyboard with live screen-reader announcements. Column limits, tags, and descriptions are supported per card, and every move emits an `arc-card-move` event so the consumer can sync its source of truth.
<arc-kanban>Overview
Kanban renders a horizontally scrolling row of columns from a single columns array. Each column has an id, a title, an optional work-in-progress limit, and an items array of cards. Cards show a label, an optional two-line description, and an optional arc-tag chip. When a column has a limit, the header count renders as count/limit and switches to the error color when the column is over its limit.
Dragging is pointer-based: press and move a card to lift it into a floating ghost that follows the cursor, with a horizontal indicator line showing exactly where the card will land. Dragging near the left or right edge of the board auto-scrolls it so long boards remain reachable. The component applies the move to its own internal copy immediately for instant feedback and emits arc-card-move with the card id, source column, target column, and final index. Listen to that event to update your actual data store, then pass the new array back in.
The keyboard model follows the accepted accessible kanban pattern: each column's card list is a single tab stop (roving tabindex), ArrowUp/ArrowDown move focus between cards, and ArrowLeft/ArrowRight jump between columns. Enter or Space grabs the focused card, arrows then move it within and across columns, Enter drops it (emitting the same arc-card-move event), and Escape cancels and returns the card to where it started. Every grab, move, drop, and cancel is announced through a polite live region.
Guidelines
When to use
- Give every column and card a stable, unique id. Moves and rendering are keyed on them
- Listen to arc-card-move and update your source-of-truth data, then pass the new array back into columns
- Set a limit on columns where work-in-progress caps matter. The badge flags overruns automatically
- Keep card labels short and put detail in the description. It clamps to two lines
- Use tag variants (primary, success, error, ...) to encode card category at a glance
When not to use
- Do not mutate the columns array in place and expect a re-render. Assign a new array instead
- Do not rely on the component as the source of truth. Its internal copy is for immediate feedback only
- Do not put interactive controls (buttons, links) inside card labels. The whole card is the drag/keyboard target
- Do not use kanban for a single static list. arc-sortable-list or arc-list is a better fit
- Do not exceed a handful of columns without expecting horizontal scrolling. Columns have a fixed 280px width
Features
- Data-driven: one columns array renders the whole board. No manual markup per card
- Pointer drag between and within columns with a floating drag ghost
- Horizontal drop indicator line between cards shows the exact insertion point
- Automatic horizontal board scrolling when dragging near the edges
- Full keyboard move protocol: Enter/Space grabs, arrows move, Enter drops, Escape cancels
- One tab stop per column (roving tabindex). No tab-key marathons through every card
- `aria-live` announcements for every grab, move, drop, and cancel
- Optional per-column WIP limit with count/limit badge that turns error-colored when exceeded
- Optional card description with a two-line clamp and an `arc-tag` chip per card
- Empty columns render a subtle dashed drop zone that highlights during drag
- `arc-card-move` and `arc-card-click` events for syncing external state
- Styleable via ::part (board, column, column-header, card, and more)
Preview
Usage
This component requires JavaScript. No pure HTML/CSS version is available. Use the Web Component directly or a framework wrapper.
<arc-kanban id="board"></arc-kanban>
<script type="module">
import '@arclux/arc-ui/kanban';
const board = document.querySelector('#board');
let columns = [
{ id: 'todo', title: 'To Do', items: [
{ id: 't1', label: 'Design onboarding flow', description: 'Draft wireframes for the first-run experience.', tag: 'Design', variant: 'secondary' },
{ id: 't2', label: 'Audit color tokens' }
]},
{ id: 'doing', title: 'In Progress', limit: 2, items: [
{ id: 'd1', label: 'Refactor auth middleware', tag: 'Backend', variant: 'primary' }
]},
{ id: 'done', title: 'Done', items: [
{ id: 'x1', label: 'Set up CI pipeline', tag: 'Infra' }
]}
];
board.columns = columns;
// Sync the source of truth from move events
board.addEventListener('arc-card-move', (e) => {
const { cardId, fromColumn, toColumn, index } = e.detail;
columns = moveCard(columns, cardId, fromColumn, toColumn, index);
});
</script>import { useState } from 'react';
import { Kanban } from '@arclux/arc-ui-react';
const initial = [
{ id: 'todo', title: 'To Do', items: [
{ id: 't1', label: 'Design onboarding flow', description: 'Draft wireframes for the first-run experience.', tag: 'Design', variant: 'secondary' },
{ id: 't2', label: 'Audit color tokens' }
]},
{ id: 'doing', title: 'In Progress', limit: 2, items: [
{ id: 'd1', label: 'Refactor auth middleware', tag: 'Backend', variant: 'primary' }
]},
{ id: 'done', title: 'Done', items: [
{ id: 'x1', label: 'Set up CI pipeline', tag: 'Infra' }
]}
];
export function Board() {
const [columns, setColumns] = useState(initial);
return (
<Kanban
columns={columns}
onArcCardMove={(e) => {
const { cardId, fromColumn, toColumn, index } = e.detail;
setColumns((cols) => moveCard(cols, cardId, fromColumn, toColumn, index));
}}
onArcCardClick={(e) => openCardDetail(e.detail.cardId)}
/>
);
}<script setup>
import { ref } from 'vue';
import { Kanban } from '@arclux/arc-ui-vue';
const columns = ref([
{ id: 'todo', title: 'To Do', items: [
{ id: 't1', label: 'Design onboarding flow', tag: 'Design', variant: 'secondary' }
]},
{ id: 'doing', title: 'In Progress', limit: 2, items: [
{ id: 'd1', label: 'Refactor auth middleware', tag: 'Backend', variant: 'primary' }
]},
{ id: 'done', title: 'Done', items: [] }
]);
function onMove(e) {
const { cardId, fromColumn, toColumn, index } = e.detail;
columns.value = moveCard(columns.value, cardId, fromColumn, toColumn, index);
}
</script>
<template>
<Kanban :columns="columns" @arc-card-move="onMove" />
</template><script>
import { Kanban } from '@arclux/arc-ui-svelte';
let columns = [
{ id: 'todo', title: 'To Do', items: [
{ id: 't1', label: 'Design onboarding flow', tag: 'Design', variant: 'secondary' }
]},
{ id: 'doing', title: 'In Progress', limit: 2, items: [
{ id: 'd1', label: 'Refactor auth middleware', tag: 'Backend', variant: 'primary' }
]},
{ id: 'done', title: 'Done', items: [] }
];
function onMove(e) {
const { cardId, fromColumn, toColumn, index } = e.detail;
columns = moveCard(columns, cardId, fromColumn, toColumn, index);
}
</script>
<Kanban {columns} on:arc-card-move={onMove} />import { Component } from '@angular/core';
import { Kanban } from '@arclux/arc-ui-angular';
@Component({
imports: [Kanban],
template: `
<arc-kanban [columns]="columns" (arcCardMove)="onMove($event)"></arc-kanban>
`,
})
export class BoardComponent {
columns = [
{ id: 'todo', title: 'To Do', items: [
{ id: 't1', label: 'Design onboarding flow', tag: 'Design', variant: 'secondary' }
]},
{ id: 'doing', title: 'In Progress', limit: 2, items: [
{ id: 'd1', label: 'Refactor auth middleware', tag: 'Backend', variant: 'primary' }
]},
{ id: 'done', title: 'Done', items: [] }
];
onMove(e: CustomEvent) {
const { cardId, fromColumn, toColumn, index } = e.detail;
this.columns = moveCard(this.columns, cardId, fromColumn, toColumn, index);
}
}API
columnsArray<{id:string,title?:string,limit?:number,items:Array<{id:string,label:string,description?:string,tag?:string,variant?:string}>}>[]- The data array that drives the board. Each entry becomes a column with a header (title plus count badge) and a list of cards.
limitrenders the count ascount/limitand turns it error-colored when exceeded. Each card needs a uniqueidand alabel;descriptionrenders below the label with a two-line clamp, andtagrenders an arc-tag chip styled byvariant. Set as a property, or as a JSON attribute for a board that is static. The component works on an internal copy for immediate drag feedback; sync your source of truth fromarc-card-moveand assign a new array to re-render. disabledbooleanfalse- Disables all pointer and keyboard interaction and dims the board.
Events
arc-card-move- Fired when a card is dropped in a new position (pointer or keyboard). detail: { cardId, fromColumn, toColumn, index } where index is the final position in the target column.
arc-card-click- Fired when a card is clicked without being dragged. detail: { cardId, columnId }.
See Also
- Sortable ListDrag-and-drop reorderable list with grip handles, keyboard reordering support, and visual insertion indicators.
- Data GridA spreadsheet-grade grid for working with tabular data: inline cell editing, multi-column sorting, pinned columns, row selection, and virtualized rendering. Columns are defined as a JavaScript array, and the grid implements the full WAI-ARIA grid keyboard pattern with a single tab stop.
- TagCompact pill-shaped label with color variants, custom color support, and an optional remove button, for categorisation, filtering, and selection feedback.