Installation
composer require wezlo/filament-kanban
Register the plugin in your PanelProvider:
FilamentKanbanPlugin::make(),
Enable Kanban on a Resource
Add HasKanbanBoard trait to your resource and define the kanban() method:
use Wezlo\FilamentKanban\Concerns\HasKanbanBoard;
class TaskResource extends Resource
{
use HasKanbanBoard;
public static function kanban(): Kanban
{
return Kanban::make()
->columns([
'todo' => Column::make('To Do'),
'in_progress' => Column::make('In Progress'),
'done' => Column::make('Done'),
])
->statusField('status'); // Your status field (enum or string)
}
}
First Use Case
Replace the default table view with the Kanban board by setting view in your table() method:
public static function table(Table $table): Table
{
return $table->view('filament-kanban::board');
}
Enum-Based Columns (Recommended)
Define a KanbanStatusEnum with transitions and WIP limits:
use Wezlo\FilamentKanban\Enums\KanbanStatusEnum;
enum TaskStatus: string implements KanbanStatusEnum
{
case TODO = 'todo';
case IN_PROGRESS = 'in_progress';
case DONE = 'done';
public function transitions(): array
{
return [
self::TODO => [self::IN_PROGRESS],
self::IN_PROGRESS => [self::TODO, self::DONE],
self::DONE => [self::IN_PROGRESS],
];
}
public function wipLimit(): int|null
{
return match($this) {
self::IN_PROGRESS => 3,
default => null,
};
}
}
Use it in kanban():
Kanban::make()
->columns(TaskStatus::cases())
->statusField('status');
Relationship-Based Columns
For dynamic columns tied to a relationship (e.g., project):
Kanban::make()
->columns(fn () => Project::query()->pluck('name', 'id'))
->statusField('project_id');
Card Actions Customize card interactions:
Kanban::make()
->cardClickAction(KanbanCardClickAction::Modal) // or SlideOver, Custom
->cardFooterActions([
KanbanCardFooterAction::Edit,
KanbanCardFooterAction::Delete,
KanbanCardFooterAction::Url('view', fn ($record) => route('tasks.view', $record)),
]);
Column-Specific Actions Add actions to column headers (e.g., "Create Task"):
Kanban::make()
->columnHeaderActions([
'todo' => [
KanbanColumnHeaderAction::Make('Create Task')
->form([
TextInput::make('title'),
])
->mutateFormDataUsing(fn (array $data) => [
'status' => 'todo',
...$data,
]),
],
]);
Search and Filters Enable search with relationship support:
Kanban::make()
->searchable(['title', 'description'])
->searchRelationships(['assigned_to']);
Authorization
Restrict moves with canMove():
Kanban::make()
->canMoveUsing(fn ($record, $from, $to) => auth()->user()->can('update-task', $record));
Hybrid Views Combine Kanban with other Filament features (e.g., tabs):
public static function table(Table $table): Table
{
return $table
->view('filament-kanban::board')
->tabs([
'kanban' => Tab::make('Kanban'),
'list' => Tab::make('List View'),
]);
}
Custom Views Publish and override Blade views:
php artisan vendor:publish --tag=filament-kanban-views
Extend resources/views/vendor/filament-kanban/board.blade.php.
Server-Side Logic
Handle moves in a KanbanMoveHandler:
Kanban::make()
->moveHandler(KanbanMoveHandler::make()
->onMove(fn ($record, $from, $to) => $record->update(['status' => $to]))
);
WIP Limits Enforce limits server-side:
Kanban::make()
->wipLimitHandler(KanbanWipLimitHandler::make()
->onExceed(fn ($column, $count) => notify('WIP limit exceeded for ' . $column)
);
Accessibility Ensure keyboard navigation by using the default ARIA attributes or extending the published views.
Status Field Mismatch
statusField doesn’t match the enum/column names.statusField() returns the exact field name used in your model (e.g., status, task_status).WIP Limits Not Triggering
wipLimitHandler() is registered and the canMove() callback respects limits:
->canMoveUsing(fn ($record, $from, $to) => $to->wipLimit() === null || $to->records()->count() < $to->wipLimit())
Relationship-Based Columns Caching
->columns(fn () => Project::query()->where('active', true)->pluck('name', 'id'))
SortableJS Conflicts
LocalStorage Persistence
localStorage isn’t blocked (e.g., in incognito mode) or use a fallback:
Kanban::make()
->persistCollapsedColumns(false) // Disable if needed
Authorization Bypass
canMove() returning false.Filament::authorize() is not overriding the handler. Use:
->canMoveUsing(fn ($record, $from, $to) => auth()->user()->can('update', $record))
Log Moves
Add debug logs to the moveHandler:
->moveHandler(KanbanMoveHandler::make()
->onMove(fn ($record, $from, $to) => Log::debug("Moved from {$from} to {$to}", ['record' => $record->id]))
)
Check Network Requests
POST /filament-kanban/move.Disable JavaScript
Clear LocalStorage
localStorage.removeItem('filament-kanban-collapsed-columns');
Custom Card Views Override the card template:
Kanban::make()
->cardView('custom.card')
->cardViewData(fn ($record) => ['extra' => 'data']);
Dynamic Column Headers Use a closure for column titles/actions:
->columns([
'todo' => Column::make(fn () => __('To Do ({$this->count} items)')),
])
Event Listeners
Listen to Kanban events (e.g., kanban.moved):
event(new KanbanMoved($record, $from, $to));
Testing
Use the KanbanTestCase helper:
use Wezlo\FilamentKanban\Testing\KanbanTestCase;
public function testKanbanMove()
How can I help you explore Laravel packages today?