Kanban
Drag-and-drop workflow board shell with columns and task cards.
Overview
The Kanban Board organizes records as cards grouped into workflow columns. Users move cards between columns to update status, priority, ownership, or workflow stage. It is a visual pattern, not a single interactive component with built-in drag-and-drop.
Byteion provides the HTML structure, semantic token classes, and responsive layout. Drag-and-drop can be connected via SortableJS, native HTML5 drag events, framework-specific adapters (dnd-kit, Vue Use gestures), or any library that provides reliable pointer and touch behaviour. Keyboard movement requires an explicit accessible fallback — see the Accessibility section.
Kanban is used across project management, CRM deal pipelines, support ticketing, content publishing, hiring pipelines, ecommerce order fulfilment, ERP/POS operational stages, and internal approval flows. Your application handles persistence, permissions, real-time sync, workflow rules, and backend validation.
When to use
- Items move through clear workflow stages
- Users need to see work distribution and bottlenecks
- Tasks or cards need owners, labels, priority, or due dates
- Project, CRM, support, content, or hiring workflows
- WIP limits or stage gating matters
When not to use
- Data is mostly tabular — use Data Table
- Users need dense sorting and filtering across hundreds of fields
- Workflow stages are unclear or change frequently
- Users mostly need reporting and analytics, not task movement
- Columns would exceed 6–8, making horizontal scroll impractical
Basic example
Three-column kanban board with To Do, In Progress, and Done. Each column has a header with card count, an add button, and cards with title, label, assignee, and due date. All classes use Byteion semantic tokens.
To Do
3User authentication flow
Implement OAuth login and session recovery.
Fix sidebar collapse on mobile
Close navigation drawer when tapping outside.
Write integration tests
Cover auth, payments, and notification flows.
In Progress
2Dashboard analytics widgets
Build interactive KPI modules for the dashboard.
API rate limiting
Add throttling per API key for public endpoints.
Done
2Sidebar navigation redesign
New compact and dark sidebar modes validated.
Database migration script
Migrate users table to support SSO identities.
Advanced examples
A. Priority card with urgent badge
Cards with border-l-danger to indicate urgent priority and a visible "Urgent" label so priority is not communicated by colour alone.
<article data-bi-kanban-card="8" draggable="true"
aria-label="Card: Payment system outage. Label: Urgent. Due: Today."
tabindex="0"
class="cursor-grab rounded-xl border border-border border-l-danger bg-card p-3 shadow-sm active:cursor-grabbing focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40">
<div class="flex items-center gap-2">
<span class="inline-flex rounded-badge bg-danger/10 px-2 py-0.5 text-[11px] font-semibold text-danger">Urgent</span>
</div>
<h4 class="mt-2 text-sm font-semibold text-foreground">Payment system outage</h4>
<p class="mt-1 text-xs text-muted-foreground">Investigate and resolve payment gateway timeout.</p>
<div class="mt-3 flex items-center justify-between text-xs text-muted-foreground">
<span class="flex items-center gap-1"><i class="ti ti-calendar text-[10px]"></i>Today</span>
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full bg-danger/10 text-[10px] font-semibold text-danger">AB</span>
</div>
</article>
B. Column with WIP limit
Display current count against the limit. Show a warning when the limit is reached or exceeded.
<div class="w-72 shrink-0" data-bi-kanban-column="in-progress" data-wip-limit="3">
<div class="mb-3 flex items-center justify-between rounded-2xl bg-primary/10 px-3 py-2.5">
<div class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full bg-primary"></span>
<h3 class="text-sm font-semibold text-foreground">In Progress</h3>
<span class="rounded-full bg-warning/10 px-2 py-1 text-xs font-semibold text-warning">3 / 3</span>
</div>
</div>
<div class="mb-2 flex items-center gap-1.5 rounded-lg bg-warning/10 px-2 py-1 text-[10px] font-medium text-warning">
<i class="ti ti-alert-triangle text-[10px]"></i>WIP limit reached
</div>
<div data-bi-kanban-dropzone class="min-h-16 space-y-3 rounded-2xl border border-dashed border-border bg-muted/40 p-2">
<!-- Cards here -->
</div>
</div>
C. Empty column state
A column with no cards should show a clear empty state so it is distinguishable from a loading or error state.
<div class="w-72 shrink-0" data-bi-kanban-column="backlog">
<div class="mb-3 flex items-center justify-between rounded-2xl bg-muted px-3 py-2.5">
<div class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full bg-muted-foreground"></span>
<h3 class="text-sm font-semibold text-foreground">Backlog</h3>
<span class="rounded-full bg-card px-2 py-1 text-xs font-semibold text-muted-foreground">0</span>
</div>
</div>
<div data-bi-kanban-dropzone class="flex min-h-24 items-center justify-center rounded-2xl border border-dashed border-border bg-muted/40 p-4">
<p class="text-xs text-muted-foreground">No tasks yet</p>
</div>
</div>
D. Loading / saving state
During an API save, the moved card should show reduced opacity and a saving indicator to prevent double interaction.
<article data-bi-kanban-card="9" aria-label="Card: Update docs. Saving…"
class="rounded-xl border border-border bg-card p-3 shadow-sm opacity-60 pointer-events-none">
<div class="flex items-center gap-2">
<span class="inline-flex rounded-badge bg-primary/10 px-2 py-0.5 text-[11px] font-semibold text-primary">Task</span>
<span class="flex items-center gap-1 text-[10px] text-muted-foreground">
<svg class="h-3 w-3 animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
Saving…
</span>
</div>
<h4 class="mt-2 text-sm font-semibold text-foreground">Update docs</h4>
</article>
State system
| State | Visual | Behaviour / API |
|---|---|---|
| default | border-border bg-card shadow-sm | Resting, interactive |
| hover | hover:shadow-md | Visual only |
| focus-visible | focus-visible:ring-2 focus-visible:ring-primary/40 | Keyboard navigation |
| dragging | cursor-grabbing + opacity | Card follows pointer; original position shows placeholder |
| drop target | Column border changes to border-primary | Visual only |
| disabled / read-only | opacity-60 pointer-events-none | Cards not draggable; board visible but not interactive |
| saving move | opacity-60 + spinner text "Saving…" | Card disabled during API request |
| failed move / rollback | Card returns to original column; Toast shows error | Roll back UI state; enable interactions |
| empty column | Centred "No tasks yet" in drop zone | Still accepts drops |
| WIP limit reached | Warning banner: bg-warning/10 text-warning | Prevent or confirm drops that exceed limit |
Data model
A recommended TypeScript model for kanban board data. Columns and cards are normalized for predictable state management.
type Priority = 'low' | 'medium' | 'high' | 'urgent';
interface KanbanColumn {
id: string;
title: string;
/** Optional WIP limit. 0 or undefined means no limit. */
limit?: number;
/** Server-sorted position for column ordering. */
position?: number;
}
interface KanbanCard {
id: string;
columnId: string;
title: string;
description?: string;
priority?: Priority;
assignee?: string;
dueDate?: string;
labels?: string[];
/** Server-sorted position within the column. */
position?: number;
}
interface KanbanBoard {
columns: KanbanColumn[];
cards: KanbanCard[];
}
Implementation guidance
- Keep board state in one source of truth — a single array or normalized store of columns and cards.
- On drop, update the UI optimistically (move the card to the target column).
- Send an API request to persist
columnIdandpositionon the server. - Roll back the UI state if the API request fails. Show an error via Toast or Alert.
- Validate WIP limits before saving — prevent the drop if the target column is at capacity.
- Filter cards by assignee, label, priority, or search text without mutating the underlying data.
- In Laravel, save
card.column_idandcard.sort_orderin the database. Use a migration to add these columns if they do not exist. - In React or Vue, keep columns and cards normalized in a
Mapor dictionary if the board grows beyond a few dozen cards.
Accessibility
- Drag-and-drop must not be the only way to move cards. Provide keyboard alternatives such as move buttons, a move-to-column menu, or card action dropdown.
- SortableJS can simplify pointer and touch dragging, but keyboard movement still requires an explicit accessible fallback. SortableJS does not provide keyboard accessibility out of the box.
- Use descriptive
aria-labelon draggable cards, e.g.aria-label="Card: User authentication flow. Label: Feature. Due: Apr 12. Assignee: SM.". - Use semantic headings (
<h3>) for column titles so screen reader users can navigate between columns. - Use
role="status"oraria-live="polite"to announce moves, saves, and errors to screen readers. - Preserve keyboard focus after a move — ideally return focus to the moved card or the nearest available card.
- Do not communicate priority by colour alone. Always include a text label such as "Urgent", "High", or "Low".
- Ensure all card actions (edit, move, delete) are keyboard-reachable through native
<button>or<a>elements. - Avoid making only a
<div>clickable for primary card actions — use semantic interactive elements. - Keep visible focus states (
focus-visible:ring-2) on all interactive card elements.
Performance guidance
- For small boards (fewer than 50 cards), render all columns and cards — no special optimization needed.
- For 100+ cards, keep card content lightweight. Avoid rich text editors or complex widgets inside card previews.
- For very large boards (500+ cards), paginate or archive old cards, or virtualize columns and cards using a library like
react-virtualorvue-virtual-scroller. - Do not render hidden card detail modals for every card on page load. Mount a single modal and populate it dynamically when the user triggers an action.
- Lazy-load card detail panels, drawers, or modals when the user clicks or taps a card.
- Debounce search and filter inputs at 250–400ms to avoid re-rendering on every keystroke.
Practical scenarios
Do and Don't
Do
- Keep columns limited and meaningful (4–6 recommended)
- Use WIP limits for active stages to expose bottlenecks
- Provide keyboard move alternatives — a move-to-column menu or action buttons
- Save card order and column after every move
- Use semantic tokens for labels, priority, and surfaces
- Show empty, loading, saving, and error states
- Make card actions keyboard-accessible
Don't
- Use Kanban for dense tabular data — use Data Table instead
- Rely only on drag-and-drop for card movement
- Communicate priority by colour only — include text labels
- Render a modal for every card on page load
- Let columns grow endlessly without filters or archiving
- Claim keyboard accessibility from a drag library unless explicitly tested
- Use raw colour classes (
bg-blue-500,text-white) instead of semantic tokens
Developer tips
Start with the data model. Define columns and card types before building the UI. A clear data model makes drag-drop logic, persistence, and filtering easier.
Keep card content short. A kanban card is a preview — show title, labels, assignee, and due date. Use a modal or drawer for full details.
Use a card detail modal for long descriptions. Clicking or double-clicking a card can open a modal with full description, comments, attachments, and activity log.
Test keyboard and touch behaviour. Drag-and-drop must work with mouse, keyboard fallback, and touch. Test on both desktop and mobile viewports.
Test failed API saves. Simulate a network failure and verify that the card returns to its original column and a helpful error is shown.
Use optimistic UI carefully. Optimistic moves feel instant, but always implement rollback. Without rollback, a failed save leaves the board in an incorrect state.
Keep examples copy-paste friendly. All examples on this page use Byteion semantic tokens. Paste them directly into a Byteion project.
Related
- Card — card surface used in kanban columns
- Badge — labels, priority, and status indicators
- Avatar — assignee initials display
- Button — add card and header actions
- Dropdown — move-to-column menu and card actions
- Modal — card detail view
- Data Table — tabular alternative to kanban
- State System — loading, saving, error states
- Accessibility — keyboard navigation and ARIA
- Performance Tips — large board optimization
- Composition Rules — combining kanban with other components
Runtime events
Byteion keeps framework APIs idiomatic: HTML uses DOM custom events, React uses callback props, and Vue uses emitted events / v-model updates.
HTML / JS events
bi:kanban:card-movebi:kanban:drag-endbi:kanban:drag-start
React callbacks
onColumnsChange(columns: KanbanColumn[]) => voidonCardMove(info: { cardId: stringonAddCard(columnId: string) => void
Vue emits
cardMove[payload: KanbanMovePayload]update:columns[columns: KanbanColumn[]]addCard[columnId: string]
Remote / AJAX
This component does not implement Byteion JS remote loading. Use normal HTML form submission, framework state, or your application data layer around it.
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.
Explore Pro Components