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.

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

StateVisualBehaviour / API
defaultborder-border bg-card shadow-smResting, interactive
hoverhover:shadow-mdVisual only
focus-visiblefocus-visible:ring-2 focus-visible:ring-primary/40Keyboard navigation
draggingcursor-grabbing + opacityCard follows pointer; original position shows placeholder
drop targetColumn border changes to border-primaryVisual only
disabled / read-onlyopacity-60 pointer-events-noneCards not draggable; board visible but not interactive
saving moveopacity-60 + spinner text "Saving…"Card disabled during API request
failed move / rollbackCard returns to original column; Toast shows errorRoll back UI state; enable interactions
empty columnCentred "No tasks yet" in drop zoneStill accepts drops
WIP limit reachedWarning banner: bg-warning/10 text-warningPrevent 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 columnId and position on 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_id and card.sort_order in 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 Map or 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-label on 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" or aria-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-virtual or vue-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

Project sprint board — Backlog, To Do, In Progress, Review, Done. WIP limit on In Progress. Cards show assignee, due date, and priority.
CRM deal pipeline — Lead, Qualified, Proposal, Negotiation, Closed Won / Closed Lost. Cards show deal value, contact, and stage probability.
Content publishing workflow — Draft, Review, Scheduled, Published, Archived. Cards show author, editor, publish date, and content type.
Support ticket queue — New, Assigned, In Progress, Waiting on Customer, Resolved. Cards show priority, customer, ticket age, and SLA status.
Hiring pipeline — Applied, Screening, Phone Interview, On-site, Offer, Hired. Cards show candidate name, role, recruiter, and stage duration.
Ecommerce order workflow — Pending, Picked, Packed, Shipped, Delivered, Returned. Cards show order ID, items, customer, and shipping method.
ERP approval workflow — Submitted, Manager Review, Director Approval, Approved, Rejected. Cards show requester, amount, department, and submission date.
Internal bug triage — Reported, Triaged, In Progress, Fixed, Verified, Closed. Cards show severity, reporter, affected version, and fix branch.
POS kitchen order board — New Order, Preparing, Ready, Served. Cards show table number, items, modifications, and order time.

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.

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-move
  • bi:kanban:drag-end
  • bi:kanban:drag-start

React callbacks

  • onColumnsChange(columns: KanbanColumn[]) => void
  • onCardMove(info: { cardId: string
  • onAddCard(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