Kanban
A board-and-column layout for task management, pipelines, and workflow visualization. Supports horizontal scrolling, WIP limits, intent-based column accents, and drag-and-drop integration.
Basic Usage
Intent-Based Column Accents
Color the top border to visually distinguish column stages:
WIP Limits
Set a limit per column. The count badge turns danger when the column exceeds capacity:
Column Footer
Add actions (like "Add item") below the cards:
Custom Column Header
Replace the default header with your own content:
Vertical Orientation
Stack columns vertically instead of horizontally:
Reordering
Add sortable to the board and to each column you want to reorder. Cards can then be moved by dragging or by keyboard.
Give each card a data-sortable-id so the new order means something to your application. Without one, a card falls back to its position — which only works for a list your server addresses positionally.
What you receive
When a move finishes, the column dispatches one bubbling wirekit:sortable:reordered event:
{{-- 1. The event bubbles, so listen anywhere above the board. --}}
<div wire:wirekit:sortable:reordered="reorder($event.detail.order)">
<x-wirekit::kanban sortable>
{{-- 2. Each card names itself; the event carries these ids. --}}
<x-wirekit::kanban-column label="Backlog" sortable>
@foreach($tasks as $task)
<x-wirekit::card :data-sortable-id="$task->id">
<x-wirekit::card.body>{{ $task->title }}</x-wirekit::card.body>
</x-wirekit::card>
@endforeach
</x-wirekit::kanban-column>
</x-wirekit::kanban>
</div>
// 3. The whole sequence arrives at once, so persist it as one statement.
public function reorder(array $order): void
{
foreach ($order as $position => $id) {
Task::whereKey($id)->update(['position' => $position]);
}
}
The payload is { order, id, from, to }: the complete new sequence, plus which card moved and where from and to.
One event, at the end — not a swap per crossing. A stream of swaps makes the final state depend on the order the responses arrive in, which is network timing: wrong whenever two are in flight, and impossible to test. A drag cannot promise the order of its own intermediate steps, so it does not report them.
Kanban Board Props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation |
string |
'horizontal' |
Layout direction: horizontal or vertical |
sortable |
bool |
false |
Enable drag-and-drop data attributes |
Kanban Column Props
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string|null |
null |
Column heading text. It also names the column's scrollable body and makes it a screen-reader landmark — see Accessibility |
count |
int |
null |
Number of items (shown as badge) |
intent |
string |
'neutral' |
Top border accent: neutral, primary, success, warning, danger, info |
limit |
int |
null |
WIP limit (badge turns danger when exceeded) |
sortable |
bool |
false |
Enable drag-and-drop data attributes |
Column Slots
| Slot | Description |
|---|---|
| default | Card items inside the column |
header |
Replace the default column header |
footer |
Action area below the cards |
Accessibility
- Board:
role="list"for the column collection - Column:
<section role="listitem">witharia-labelledbylinking to the column label - Sortable: reordering is operable by keyboard as well as by pointer — see below
- Each card is focusable and carries
aria-roledescription="Sortable item"while a column is sortable - Column body:
tabindex="0"and a focus ring in every configuration, so the scrolling card list is keyboard-reachable even when the cards inside hold no other focusable element (WCAG 2.1.1) - The column body becomes a screen-reader landmark only when the column has a
label. It is then arole="region"under that same name. Without one it stays a plain focusable scroller —role="region"plus a name is a landmark, and a six-column board falling back to one built-in name would be six landmarks announcing the same thing, which axe reports aslandmark-unique. Every column on this page is labeled, which is the shape to copy.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus through cards |
Space / Enter |
Lift the focused card, or drop the one you lifted |
↓ → |
Move the lifted card one place later |
↑ ← |
Move the lifted card one place earlier |
Escape |
Abandon the move and put the card back |
Reordering by keyboard is part of the feature rather than an addition to it: a list that can only be reordered by dragging cannot be reordered at all by someone not using a pointer.
Styling a card while it moves
A card carries a state attribute for as long as it is in motion, so you can give it whatever treatment fits your board. Both are removed when the move ends or is abandoned:
| Attribute | While |
|---|---|
data-sortable-dragging |
The card is being dragged with a pointer |
data-sortable-lifted |
The card has been lifted with the keyboard and is being moved |
/* 1. The two states are separate on purpose: a dragged card is under the
pointer and already reads as "held", a lifted one has only the keyboard
to say so and needs to say it more loudly. */
[data-sortable-dragging] { opacity: 0.5; }
[data-sortable-lifted] { outline: 2px solid var(--color-wk-ring); }
Pitfalls
- Give every card a
data-sortable-id. Positional fallbacks are silent and correct until the list is filtered or paginated. - Persist the sequence you are handed, not a swap. The event gives you the whole order for exactly this reason.
Design Tokens
| Token | Used for |
|---|---|
--font-wk-sans |
Kanban font family |
--font-wk-heading-weight |
Column title weight |
--text-wk-sm |
Column title + count badge font size |
--color-wk-text |
Column title |
--color-wk-bg-muted |
Column body background |
--color-wk-border / --color-wk-border-subtle |
Column border + separator |
--color-wk-accent / --color-wk-success / --color-wk-warning / --color-wk-danger |
Top-border accent per intent |
--color-wk-ring / --color-wk-ring-offset |
Focus ring |
--ring-wk-width / --ring-wk-offset |
Focus ring geometry |
--border-wk-width |
Border width |
--radius-wk-lg |
Column border radius |