Introduction
The Kanban component provides a flexible board interface for organizing tasks, workflows, and projects into columns. With extensive customization through slots and a clean API, it's perfect for project management, workflow visualization, and any column-based organization system.
Installation
Use the Sheaf artisan command to install the kanban component:
php artisan sheaf:install kanban
Once installed, you can use
<x-ui.kanban />,<x-ui.kanban.column />,<x-ui.kanban.header />,<x-ui.kanban.cards />, and<x-ui.kanban.card />components in any Blade view.
Basic Usage
Axioms
Lemmas
Proven Theorems
<x-ui.kanban> <x-ui.kanban.column> <x-ui.kanban.header :count="6"> <x-ui.heading>Axioms</x-ui.heading> <x-ui.text>Foundational statements</x-ui.text> </x-ui.kanban.header> <x-ui.kanban.cards> <x-ui.kanban.card :id="1"> <x-ui.text class="font-semibold"> Parallel Postulate </x-ui.text> <x-ui.text class="opacity-60 text-xs"> Through a point not on a line, exactly one parallel line exists </x-ui.text> </x-ui.kanban.card> <!-- More cards... --> </x-ui.kanban.cards> </x-ui.kanban.column> <x-ui.kanban.column> <x-ui.kanban.header :count="5"> <x-ui.heading>Lemmas</x-ui.heading> <x-ui.text>Helper theorems</x-ui.text> </x-ui.kanban.header> <x-ui.kanban.cards> <!-- Cards... --> </x-ui.kanban.cards> </x-ui.kanban.column> <x-ui.kanban.column> <x-ui.kanban.header> <x-ui.heading>Proven Theorems</x-ui.heading> </x-ui.kanban.header> <x-ui.kanban.cards> <x-slot:empty> <p class="text-sm text-neutral-500">No proven theorems yet</p> </x-slot:empty> </x-ui.kanban.cards> </x-ui.kanban.column> </x-ui.kanban>
Column Width
Control column width using CSS custom properties:
Conjectures
Proof Techniques
<!-- Narrow columns (18rem) --> <x-ui.kanban class="[--column-width:18rem]"> <!-- Columns will be 18rem wide --> </x-ui.kanban> <!-- Wide columns (24rem) --> <x-ui.kanban class="[--column-width:28rem]"> <!-- Columns will be 28rem wide --> </x-ui.kanban> <!-- (20rem is the default) -->
Card Variations
Card with Top and Bottom Slots
Add metadata above and below the main content, (this helps organize things out more than just optional):
Complexity Classes
<x-ui.kanban.card> <x-slot:top> <!-- Content above main card content --> <div class="flex items-center justify-between mb-2"> <x-ui.badge color="green" size="sm">P</x-ui.badge> <x-ui.text size="xs">Polynomial time</x-ui.text> </div> </x-slot:top> <!-- Main card content --> <div> <x-ui.text class="font-semibold">P vs NP Problem</x-ui.text> <x-ui.text size="sm">Description here</x-ui.text> </div> <x-slot:bottom> <!-- Content below main card content --> <div class="flex items-center justify-between mt-3"> <x-ui.text size="xs">Stephen Cook (1971)</x-ui.text> <x-ui.text size="xs">Millennium Prize</x-ui.text> </div> </x-slot:bottom> </x-ui.kanban.card>
Card Size Variants
Adjust card padding with size variants, use them as your condesing more contents :
Extra Small
Axiom of Extensionality
Axiom of Pairing
Axiom of Union
Small
Identity Element
a + 0 = a
Inverse Element
a + (-a) = 0
Associativity
(a+b)+c = a+(b+c)
Medium (Default)
Cauchy Sequence
For all ε > 0, there exists N such that |aₙ - aₘ| < ε for n,m > N
Limit Definition
lim f(x) = L if for all ε > 0, exists δ > 0
Continuity
f continuous at c if lim[x→c] f(x) = f(c)
<!-- Extra small cards --> <x-ui.kanban.column size="xs"> <x-ui.kanban.header> <x-ui.heading>Tasks</x-ui.heading> </x-ui.kanban.header> <x-ui.kanban.cards> <x-ui.kanban.card> <h4 class="text-xs">Compact card</h4> </x-ui.kanban.card> </x-ui.kanban.cards> </x-ui.kanban.column> <!-- Small cards --> <x-ui.kanban.column size="sm"> <x-ui.kanban.header> <x-ui.heading>Tasks</x-ui.heading> </x-ui.kanban.header> <x-ui.kanban.cards> <x-ui.kanban.card> <h4 class="text-sm">Small card</h4> </x-ui.kanban.card> </x-ui.kanban.cards> </x-ui.kanban.column> <!-- Medium cards (default) --> <x-ui.kanban.column> <x-ui.kanban.header> <x-ui.heading>Tasks</x-ui.heading> </x-ui.kanban.header> <x-ui.kanban.cards> <x-ui.kanban.card> <h4>Medium card</h4> </x-ui.kanban.card> </x-ui.kanban.cards> </x-ui.kanban.column>
Empty States
Provide custom empty states for columns with no cards:
Unsolved Problems
No problems
<x-ui.kanban.column> <x-ui.kanban.header :count="$count"> <x-ui.heading>Unsolved Problems</x-ui.heading> </x-ui.kanban.header> @if($empty) <x-ui.empty> <x-ui.empty.media class="flex items-center justify-center w-12 h-12 rounded-full bg-neutral-100 dark:bg-neutral-800" > <x-ui.icon name="x-mark" /> </x-ui.empty.media> <x-ui.empty.contents> <x-ui.heading>No problems</x-ui.heading> <x-ui.text class="opacity-70"> All problems have been solved! </x-ui.text> </x-ui.empty.contents> </x-ui.empty> @else <x-ui.kanban.cards> <!-- --> </x-ui.kanban.cards> @endif </x-ui.kanban.column>
Column Footers
Add interactive aligned elements to columns footers:
Open Problems
Riemann Hypothesis
Zeros of the Riemann zeta function
Navier-Stokes Existence
Smooth solutions for 3D fluid equations
<x-ui.kanban.column> <x-ui.kanban.header> <x-ui.heading>Tasks</x-ui.heading> </x-ui.kanban.header> <x-ui.kanban.cards> <!-- Cards go here --> </x-ui.kanban.cards> <x-ui.kanban.footer> <div class="space-y-2"> <x-ui.input/> <div class="flex gap-2"> <x-ui.button color="blue" variant="outline" size="sm" icon="plus" > Add Problem </x-ui.button> <x-ui.button variant="outline" size="sm" icon="arrow-up-tray" class="ml-auto" > Import </x-ui.button> </div> </div> </x-ui.kanban.footer> </x-ui.kanban.column>
Implementation Guide
This guide shows you how to build a fully functional kanban board with drag-and-drop reordering using Livewire. We'll create a mathematical research workflow board that tracks conjectures through review to proven theorems.
Overview
We'll build a kanban board where:
- Columns represent workflow stages (Conjectures → Under Review → Proven Theorems)
- Cards represent mathematical statements with progress tracking
- Users can drag columns to reorder workflow stages
- Users can drag cards within columns or between columns
- All reordering persists to the database
Database Setup
First, create the necessary migrations:
Board Migration:
Schema::create('boards', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); });
BoardColumn Migration:
Schema::create('board_columns', function (Blueprint $table) { $table->id(); $table->foreignId('board_id')->constrained()->cascadeOnDelete(); $table->string('title'); $table->text('description')->nullable(); $table->unsignedInteger('order')->default(0); $table->timestamps(); $table->index(['board_id', 'order']); });
MathematicalStatement Migration:
Schema::create('mathematical_statements', function (Blueprint $table) { $table->id(); $table->foreignId('column_id')->constrained('board_columns')->cascadeOnDelete(); $table->string('title'); $table->text('description'); $table->string('field'); // number_theory, topology, etc. $table->integer('year'); $table->string('status'); // conjecture, review, proven $table->unsignedInteger('progress')->default(0); // 0-100 $table->unsignedInteger('order')->default(0); $table->timestamps(); $table->index(['column_id', 'order']); });
Model Setup
Define the Eloquent relationships:
Board Model:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Board extends Model { protected $fillable = ['name']; public function columns() { return $this->hasMany(BoardColumn::class)->orderBy('order'); } }
BoardColumn Model:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class BoardColumn extends Model { protected $fillable = ['board_id', 'title', 'description', 'order']; public function board() { return $this->belongsTo(Board::class); } public function statements() { return $this->hasMany(MathematicalStatement::class, 'column_id') ->orderBy('order'); } }
MathematicalStatement Model:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class MathematicalStatement extends Model { protected $fillable = [ 'column_id', 'title', 'description', 'field', 'year', 'status', 'progress', 'order' ]; public function column() { return $this->belongsTo(BoardColumn::class, 'column_id'); } }
Using the Reorderable Trait
The kanban component ships with a powerful App\Livewire\Concerns\Reorderable trait that handles all the complex database logic for reordering items. You don't need to write the SQL yourself—just use the trait's methods.
Key Methods:
reorderWithinScope()- Reorder items within the same container (e.g., cards in the same column)moveBetweenScopes()- Move items between containers (e.g., card from one column to another)reorderTransaction()- Wrap reordering in a database transaction for safety
Livewire Component
Create your Livewire component that uses the Reorderable trait:
<?php namespace App\Livewire; use App\Livewire\Concerns\Reorderable; use App\Models\Board; use App\Models\BoardColumn; use App\Models\MathematicalStatement; use Livewire\Component; class KanbanBoard extends Component { use Reorderable; public Board $board; public function mount(): void { $this->loadBoard(); } public function render() { return view('livewire.kanban-board'); } private function loadBoard(): void { $this->board = Board::query() ->with(['columns', 'columns.statements']) ->firstOrFail(); } /** * Handle column reordering (drag columns to rearrange workflow) */ public function sortColumns(int $columnId, int $newPosition): void { $this->reorderTransaction(function () use ($columnId, $newPosition) { $column = BoardColumn::findOrFail($columnId); // Reorder within the board's columns $this->reorderWithinScope( model: $column, newPosition: $newPosition, scope: fn ($q) => $q->where('board_id', $column->board_id) ); }); $this->loadBoard(); } /** * Handle card reordering and moving between columns */ public function sortCards(int $cardId, int $newPosition, int $targetColumnId): void { $this->reorderTransaction(function () use ($cardId, $newPosition, $targetColumnId) { $card = MathematicalStatement::findOrFail($cardId); if ($card->column_id === $targetColumnId) { // Same column - just reorder $this->reorderWithinScope( model: $card, newPosition: $newPosition, scope: fn ($q) => $q->where('column_id', $card->column_id) ); } else { // Different column - move between scopes $this->moveBetweenScopes( model: $card, fromScope: fn ($q) => $q->where('column_id', $card->column_id), toScope: fn ($q) => $q->where('column_id', $targetColumnId), scopeAttributes: ['column_id' => $targetColumnId], newPosition: $newPosition ); } }); $this->loadBoard(); } }
How the Reorderable Trait Works:
reorderWithinScope(): When you drag a card from position 2 to position 5 within the same column, the trait automatically shifts all affected cards and updates theirordervaluesmoveBetweenScopes(): When you drag a card from "Conjectures" to "Under Review", the trait handles removing it from the old column, shifting remaining cards, and inserting it at the new positionreorderTransaction(): Wraps everything in a database transaction so if anything fails, no partial updates occur
Blade View Implementation
Now create the view with drag-and-drop enabled:
<div class="min-h-screen py-12 px-4"> <div class="max-w-5xl mx-auto"> <div class="text-center space-y-2 mb-8"> <h1 class="text-3xl font-bold">Mathematical Research Board</h1> <p class="text-neutral-600 dark:text-neutral-400"> Track conjectures through peer review to proven theorems </p> </div> <x-ui.kanban class="max-h-[800px]" x-sort="$wire.sortColumns" x-sort:config="{forceFallback: true}" > @foreach ($this->board->columns as $column) <x-ui.kanban.column size="sm" x-sort:item="'{{ $column->id }}'" > <x-ui.kanban.header :count="count($column->statements)"> <x-ui.heading>{{ $column->title }}</x-ui.heading> <x-ui.text>{{ $column->description }}</x-ui.text> </x-ui.kanban.header> <x-ui.kanban.cards x-data="{ handle: (item, position) => { $wire.sortCards(item, position, {{ $column->id }}) } }" x-sort:config="{forceFallback: true}" x-sort="handle" x-sort:group="board-{{ $board->id }}" > @if($column->statements->isEmpty()) <x-slot:empty> <div class="text-center py-8"> <p class="text-neutral-500">No statements yet</p> </div> </x-slot:empty> @else @foreach ($column->statements as $statement) <x-ui.kanban.card size="sm" x-sort:item="{{ $statement->id }}" > <x-slot:top> <div class="flex items-center justify-between mb-2"> @php $fieldColors = [ 'number_theory' => 'yellow', 'topology' => 'blue', 'analysis' => 'indigo', 'graph_theory' => 'pink', ]; $color = $fieldColors[$statement->field] ?? 'neutral'; @endphp <x-ui.badge variant="outline" color="{{ $color }}" size="sm" > {{ ucfirst(str_replace('_', ' ', $statement->field)) }} </x-ui.badge> <x-ui.text size="xs" class="opacity-60"> {{ $statement->year }} </x-ui.text> </div> </x-slot:top> <div> <x-ui.text class="font-semibold"> {{ $statement->title }} </x-ui.text> <x-ui.text size="xs" class="opacity-60 mt-1"> {{ $statement->description }} </x-ui.text> @if($statement->status === 'review') <div class="mt-2"> <x-ui.progress :value="$statement->progress" color="blue" size="sm" showValue label="Verification" /> </div> @endif </div> </x-ui.kanban.card> @endforeach @endif </x-ui.kanban.cards> </x-ui.kanban.column> @endforeach </x-ui.kanban> </div> </div>
Key Attributes Explained:
x-sort="$wire.sortColumns"on the board - Enables column drag-and-dropx-sort:item="'{{ $column->id }}'"- Identifies which column is being draggedx-sort="handle"on cards container - Uses custom handler for card sortingx-sort:group="board-{{ $board->id }}"- Allows cards to be dragged between columnsx-sort:item="{{ $statement->id }}"- Identifies which card is being dragged
Component Props
ui.kanban
| Prop | Type | Default | Description |
|---|---|---|---|
size |
string | 'md' |
Board size variant |
class |
string | - | Additional CSS classes |
--column-width |
CSS var | 20rem |
Width of each column (use arbitrary values like [--column-width:24rem]) |
ui.kanban.column
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | null |
Unique identifier for the column |
size |
string | 'md' |
Size variant (inherited from board or set explicitly) |
header |
slot | - | Custom header content (replaces default header) |
footer |
slot | - | Footer content at bottom of column |
ui.kanban.header
| Prop | Type | Default | Description |
|---|---|---|---|
count |
number | null |
Item count badge (displayed in header) |
ui.kanban.cards
| Prop | Type | Default | Description |
|---|---|---|---|
empty |
slot | - | Custom empty state when no cards present |
ui.kanban.card
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | null |
Unique identifier for the card |
size |
string | 'md' |
Size variant: 'xs', 'sm', 'md' (inherited from column or set explicitly) |
top |
slot | - | Content above main card content |
bottom |
slot | - | Content below main card content |
handle |
slot | - | Drag handle (shown on hover, positioned top-left) |