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 List Drag-and-drop reorderable list with grip handles, keyboard reordering support, and visual insertion indicators.
- Data Grid A 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.
- Tag Compact pill-shaped label with color variants, custom color support, and an optional remove button, for categorisation, filtering, and selection feedback.