Skip to main content
Copy for LLM

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

Simple kanban board
To Do 2
Design homepage
Write copy
In Progress 1
Build API
Done 1
Setup CI/CD

On paper, where nothing scrolls sideways, the columns of a horizontal board flow onto the next line, so every column prints. To give a row of your own the same, see data-wk-print-wrap.

Intent-Based Column Accents

Color the top border to visually distinguish column stages:

Column intent variants
Backlog 4
Research competitors
Draft project brief
Schedule kickoff call
Prepare mood board
Active 2
Wireframe dashboard
API endpoint spec
Review 1
Auth flow PR
Complete 3
Repo bootstrap
CI pipeline setup
Style-guide v1

WIP Limits

Set a limit per column. The count badge turns danger when the column exceeds capacity:

WIP limit enforcement
In Progress 3/5
Refactor billing module
Localize checkout flow
Performance audit
Review 6/5
Onboarding redesign PR
Email-template refresh
Search index migration
Webhook retry logic
Audit-log viewer
Account-deletion endpoint

Add actions (like "Add item") below the cards:

Column with footer
To Do 1
Existing task

Custom Column Header

Replace the default header with your own content:

Custom header slot
Sprint 4

3 items

Plan retro agenda
Update release notes
Demo prep

Vertical Orientation

Stack columns vertically instead of horizontally:

Vertical kanban
Stage 1 2
Discovery interviews
Define success metrics
Stage 2 1
Approved roadmap

Reordering

Add sortable to the board and to each column you want to reorder. Cards can then be moved by dragging, by keyboard, or with two clicks once the column's Reorder button is pressed.

A sortable column
Backlog 3
Design homepage
Write copy
Build API

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.

Moving cards between columns

Add cross-column to a sortable board and give each column a column-id. A card can then leave its column: dragged onto another one, carried there with the arrow keys while it is lifted, or placed there with a click in reorder mode.

Cards that move between columns
To do
Write the brief
Approve the budget
In progress
Design the homepage
Done
Hold the kickoff

With the keyboard, ← and → carry a lifted card into the previous or next column, and ↑ ↓ keep moving it within one. Escape returns it to the column it started in, not only to a position in the one it happens to be in.

A move between columns is reported once, from the column the card lands in, as a bubbling wirekit:sortable:moved event:

{{-- 1. The event bubbles, so listen anywhere above the board. --}}
<div wire:wirekit:sortable:moved="moveCard($event.detail)">
    <x-wirekit::kanban sortable cross-column>
        @foreach($columns as $column)
            {{-- 2. The column-id comes back as from.column and to.column. --}}
            <x-wirekit::kanban-column :label="$column->name" :column-id="$column->key" sortable>
                @foreach($column->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>
        @endforeach
    </x-wirekit::kanban>
</div>
// 3. Both columns' new order arrives together, so one transaction covers the whole move.
public function moveCard(array $move): void
{
    DB::transaction(function () use ($move) {
        Task::whereKey($move['id'])->update(['status' => $move['to']['column']]);

        foreach (['from', 'to'] as $side) {
            foreach ($move[$side]['order'] as $position => $id) {
                Task::whereKey($id)->update(['position' => $position]);
            }
        }
    });
}

The payload is { id, from: { column, index, order }, to: { column, index, order } }. A move inside one column arrives as wirekit:sortable:reordered, as it does without cross-column, so a board that turns this on keeps its handler for that case.

Without cross-column nothing changes: the arrow keys keep moving a lifted card within its own column, and a card dragged over another column is refused. It is off by default because it changes what ← and → do.

Kanban Board Props

Prop Type Default Description
orientation string 'horizontal' Layout direction: horizontal or vertical
sortable bool false Marks the board as sortable. The cards themselves are reordered by sortable on each column, and crossColumn builds on this one
crossColumn bool false Lets a card move between sortable columns, by drag, by ← → while lifted, and by a click in reorder mode — see Moving cards between columns. Needs sortable
scope string|null null Scoped personalization name

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 Makes the cards in this column reorderable by drag, by keyboard, and by clicks in reorder mode through the Reorder button in its header
columnId string|null null What your application calls this column. It comes back as from.column and to.column when a card moves between columns; without one the column is named by its position
scope string|null null Scoped personalization name

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">, named from its label. With the default header the name is an aria-labelledby reference to the header text; when you replace the header through the header slot there is no label element of ours left to point at, so the same label becomes an aria-label instead. A column with no label is an unnamed list item — pass one.
  • Sortable: reordering is operable by keyboard, by dragging, and with single clicks in reorder mode for a pointer that cannot drag (WCAG 2.2 SC 2.5.7) — see below
  • Each card is focusable and carries aria-roledescription="Sortable item" while a column is sortable
  • On a cross-column board, a card carried into another column is announced with that column's label before its new position, and focus stays on the card
  • 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 a role="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 as landmark-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
→ Move the lifted card one place later — on a cross-column board, into the next column
← Move the lifted card one place earlier — on a cross-column board, into the previous column
Escape Abandon the move and put the card back where it started, in its own column

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.

Reordering with clicks

A sortable column shows a Reorder button in its header. Pressed, it puts the column in reorder mode, and on a cross-column board every column at once. Its accessible name carries the column's label, "Reorder To do", so a board of separately sortable columns does not offer several buttons with one name. On a cross-column board every one of them does the same thing, and each is named Reorder. In that mode:

  • A click on a card picks it up. It takes the focus and is drawn as lifted.
  • A click on another card puts the picked card in that card's place. On a cross-column board the other card can be in another column.
  • A click on a column's empty space puts the card last in that column.
  • A click on the picked card, Space or Enter puts it down where it is. Escape puts it down where it was.
  • Pressing Reorder again ends the mode, and a card still picked up is put down where it was.

This is how a card moves with a pointer that clicks but cannot drag, such as a head pointer, a mouth stick or eye tracking. While the mode is on, a click belongs to the reorder: links and handlers inside the cards do not run. A move reports once, through the same wirekit:sortable:reordered or wirekit:sortable:moved event as a drag.

Styling a card while it moves

A card carries a state attribute for as long as it is in motion. 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, or picked up with a click in reorder mode, and is being moved

A focused card wears the focus ring, and a lifted card is drawn apart from it: a dashed outline in the accent and a raised shadow. To give the lifted state a treatment of your own, name the focused state as well, because a lifted card always has the focus:

/* 1. A dragged card is under the pointer and already reads as "held". */
[data-sortable-dragging] { opacity: 0.5; }

/* 2. Your own lifted state, focused or not. */
[data-sortable-lifted],
[data-sortable-lifted]:focus-visible { outline: 2px dashed var(--color-wk-accent); outline-offset: 4px; }

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.
  • Give every column a column-id on a cross-column board. Without one a column is named by its position, which stays correct only until a column is added, hidden or reordered.
  • The count badge and the WIP limit are your server's numbers. They change when your component re-renders after saving the move, not at the moment of the drop.
  • Refuse a move by rendering the order you keep. A Livewire method that does not save the move, a full column for example, re-renders the board, and the card goes back to the place your server keeps. The column then says where the card is, "Could not save. Back at position 1 of 3.", since it had announced the drop. A request that fails without a render leaves the card where it was dropped until the next one.
  • Listen for both events on a cross-column board. A move inside a column arrives as wirekit:sortable:reordered, a move between columns as wirekit:sortable:moved, and a board that handles only one of them silently drops the other kind of move.

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

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store