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
Press Enter or Space to pick up a card, arrow keys to move it, Enter to drop, Escape to cancel.
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. `limit` renders the count as `count/limit` and turns it error-colored when exceeded. Each card needs a unique `id` and a `label`; `description` renders below the label with a two-line clamp, and `tag` renders an arc-tag chip styled by `variant`. Set via JavaScript — it is not an HTML attribute. The component works on an internal copy for immediate drag feedback; sync your source of truth from `arc-card-move` and 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 Table A data-driven table component that renders rows from a JavaScript array. Declarative column definitions via `arc-column` children control which fields appear, their headers, widths, and sort behavior. Built-in support for column sorting, row selection with checkboxes, and an empty-state fallback.
- Tag Compact pill-shaped label with color variants, custom color support, and an optional remove button, for categorisation, filtering, and selection feedback.