Easy Kanban Board
An advanced Kanban Board package for Filament. Drop it into any Resource's List page to replace the table with a fully interactive board.
Author:
Mustafa Khaled
Documentation
- Requirements
- Features
- Installation
- Quick Start
- KanbanStatusEnum Interface
- Configuration
- Custom Theme
- Testing
- License
An advanced Kanban Board package for Filament. Drop it into any Resource's List page to replace the table with a fully interactive board.
#Requirements
- PHP 8.3+
- Laravel 13+
- Filament 4+
#Features
- Drag-and-drop cards between columns (SortableJS)
- Enum-based or relationship-based columns
KanbanStatusEnuminterface for defining transitions & WIP limits on the enum itself- Card click action (modal, slide-over, or custom)
- Card footer actions (edit, delete, URL navigation, custom)
- Column header actions (create with pre-filled status)
- Filters dropdown with active count badge & reset
- Search bar with relationship support
- Collapsible columns (persisted in localStorage)
- WIP limits with visual warnings and server-side enforcement
- Column summaries (aggregates)
- Empty state per column
- Drag constraints (client-side + server-side)
canMove()callback for custom authorization- Resource policy authorization on every move
- Loading indicator
- Custom views (card, column, board)
- Dark mode support
- Accessibility (ARIA roles, labels, keyboard-friendly)
- Publishable Blade views
- Error notifications on failed moves
#Installation
composer require wezlo/filament-kanban
Register the plugin in your Panel Provider:
use Wezlo\FilamentKanban\FilamentKanbanPlugin;
->plugins([
FilamentKanbanPlugin::make(),
])
#Quick Start
Add HasKanbanBoard to your Resource's List page and define kanban():
use Wezlo\FilamentKanban\Concerns\HasKanbanBoard;
use Wezlo\FilamentKanban\KanbanBoard;
class ListLeads extends ListRecords
{
use HasKanbanBoard;
protected static string $resource = LeadResource::class;
public function kanban(KanbanBoard $kanban): KanbanBoard
{
return $kanban
->enumColumn('status', LeadStatus::class)
->cardTitle(fn ($record) => $record->title)
->cardDescription(fn ($record) => $record->assignee?->name);
}
}
The board replaces the table. Columns are generated from your enum. The breadcrumb shows "Board" instead of "List".
#KanbanStatusEnum Interface
For full integration, implement KanbanStatusEnum on your enum. This lets you define allowed transitions and WIP limits directly on the enum -- no board configuration needed.
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Icons\Heroicon;
use Wezlo\FilamentKanban\Contracts\KanbanStatusEnum;
enum LeadStatus: string implements HasIcon, KanbanStatusEnum
{
case New = 'new';
case Contacted = 'contacted';
case SiteVisit = 'site_visit';
case Negotiation = 'negotiation';
case Won = 'won';
case Lost = 'lost';
// Required by HasLabel (via KanbanStatusEnum)
public function getLabel(): string
{
return match ($this) {
self::New => 'New',
self::Contacted => 'Contacted',
// ...
};
}
// Required by HasColor (via KanbanStatusEnum)
public function getColor(): string
{
return match ($this) {
self::New => 'info',
self::Contacted => 'warning',
// ...
};
}
// Optional: HasIcon
public function getIcon(): Heroicon
{
return match ($this) {
self::New => Heroicon::Sparkles,
// ...
};
}
// Define which statuses each status can transition to.
// Return null to allow all transitions.
public function getAllowedTransitions(): ?array
{
return match ($this) {
self::New => [self::Contacted, self::Lost],
self::Contacted => [self::SiteVisit, self::Lost],
self::SiteVisit => [self::Negotiation, self::Lost],
self::Negotiation => [self::Won, self::Lost],
self::Won => null, // no constraints
self::Lost => null,
};
}
// Set max cards per column. Return null for unlimited.
public function getWipLimit(): ?int
{
return match ($this) {
self::Negotiation => 10,
default => null,
};
}
}
The board automatically reads these -- just use ->enumColumn('status', LeadStatus::class) and transitions + WIP limits are enforced both client-side and server-side.
Without the interface: Regular BackedEnum with HasLabel + HasColor still works. You just configure constraints on the board instead.
Explicit overrides: Board-level ->dragConstraints() and ->wipLimits() override enum values per column.
#Configuration
#Column Source
Enum-based (columns from a BackedEnum):
->enumColumn('status', LeadStatus::class)
Relationship-based (columns from a related model):
->relationshipColumn('stage', 'name', Stage::class, orderAttribute: 'sort_order')
#Card Content
->cardTitle(fn ($record) => $record->title)
->cardDescription(fn ($record) => $record->assignee?->name)
->cardBadges(fn ($record) => [
['label' => $record->priority->getLabel(), 'color' => $record->priority->getColor()],
])
#Card Click Action
Pass any Filament Action to fire when a card is clicked:
use Filament\Actions\Action;
use Filament\Infolists\Components\TextEntry;
->cardAction(
Action::make('view')
->slideOver()
->schema([
TextEntry::make('title'),
TextEntry::make('status')->badge(),
])
->fillForm(fn ($record) => $record->toArray())
->modalSubmitAction(false)
->modalCancelActionLabel('Close')
)
Clicking opens the modal. Dragging still works -- the package distinguishes clicks from drags using SortableJS events.
#Card Footer Actions
Icon buttons at the bottom of each card. Actions with ->url() render as links, others use Livewire modals.
use Filament\Actions\Action;
use Filament\Support\Icons\Heroicon;
->cardFooterActions([
Action::make('edit')
->icon(Heroicon::PencilSquare)
->color('gray')
->url(fn ($record) => LeadResource::getUrl('edit', ['record' => $record])),
Action::make('delete')
->icon(Heroicon::Trash)
->color('danger')
->requiresConfirmation()
->action(fn ($record) => $record->delete()),
])
#Column Header Action
"+" button per column. The column value is pre-filled into the form.
use Filament\Actions\CreateAction;
->columnHeaderAction(CreateAction::make())
#Filters
Renders as a dropdown panel triggered by a filter icon next to the search bar. Shows active filter count as a badge.
use Filament\Forms\Components\Select;
->filters([
Select::make('priority')
->options(LeadPriority::class)
->placeholder('All Priorities'),
Select::make('assigned_to')
->relationship('assignee', 'name')
->placeholder('All Assignees'),
])
->filtersColumns(2) // grid columns inside the dropdown
#Search
->searchable(['title', 'client.user.name'])
Supports dot notation for relationship columns.
#Collapsible Columns
->collapsible()
State persisted per column in localStorage.
#WIP Limits
Define on the enum via KanbanStatusEnum::getWipLimit(), or on the board:
->wipLimits(['new' => 5, 'in_progress' => 10])
->defaultWipLimit(20)
The count badge turns red when over limit. Moves into over-limit columns are blocked server-side with a notification.
#Column Summaries
->columnSummary(function ($records, $column) {
$total = $records->sum('estimated_budget');
return $total > 0 ? 'SAR ' . number_format($total, 0) : null;
})
#Empty State
->emptyState('No leads', 'Drag leads here or create a new one')
#Drag Constraints
Define on the enum via KanbanStatusEnum::getAllowedTransitions(), or on the board:
->dragConstraints([
'new' => [LeadStatus::Contacted, LeadStatus::Lost],
'contacted' => [LeadStatus::SiteVisit, LeadStatus::Lost],
])
Enforced both client-side (SortableJS put function) and server-side (before DB update).
#Authorization
Resource policy: The package checks Resource::canEdit($record) before every move. Unauthorized moves show a danger notification.
canMove callback: Custom business logic:
->canMove(function ($record, $oldStatus, $newStatus) {
if ($newStatus === 'won') {
return auth()->user()->hasRole('project-manager');
}
return true;
})
Order of checks: Resource policy -> Drag constraints -> WIP limits -> canMove callback. First failure blocks the move.
#Move Callback
Run logic after a successful move:
->onRecordMoved(function ($record, $fromValue, $toValue) {
activity()->performedOn($record)->log("Moved from {$fromValue} to {$toValue}");
})
#Query Customization
->modifyQueryUsing(fn ($query) => $query->where('company_id', auth()->user()->company_id))
->recordsPerColumn(50)
->excludeColumns([LeadStatus::Lost])
#Column Appearance
->columnWidth('320px')
->columnColor(fn ($column) => $column->color ?? 'gray')
#Custom Views
Override any view:
->cardView('leads.kanban.card') // receives $record, $board, $column
->columnView('leads.kanban.column') // receives $column, $board
->boardView('leads.kanban.board')
Or publish all views:
php artisan vendor:publish --tag=filament-kanban-views
#Loading Indicator
Enabled by default. Shows a spinner overlay during Livewire updates.
->loading(false) // disable
#Custom Theme
If you have a custom Filament theme, add the package views to your @source directive:
@source '../../../../vendor/wezlo/filament-kanban/resources/views/**/*';
#Testing
php artisan test --filter=KanbanBoard
#License
MIT
The author
From the same author
Workspace Tabs
Browser-like tabs for Filament panels. Open multiple pages in tabs without losing context, drag to reorder, pin frequently accessed pages, and right-click for quick actions.
Author:
Mustafa Khaled
Search Spotlight
A full-screen Spotlight / command-palette search overlay for Filament panels. Opens on ⌘K (configurable), aggregates results from multiple categories (records, resources, pages, actions, plus recent/pinned from localStorage), and composes Filament's built-in `GlobalSearchProvider` — every resource that already implements `getGloballySearchableAttributes()` shows up automatically.
Author:
Mustafa Khaled
Export Pro
A comprehensive, model-agnostic export engine for Filament. Handles large datasets with background processing and real-time progress tracking, maintains full export history with audit trails, supports scheduled/recurring exports with delivery, and gives admins visibility into who exported what and when.
Author:
Mustafa Khaled
Record Freezer
Freeze individual Eloquent records against modification — finalised contracts, audited financial periods, legal holds
Author:
Mustafa Khaled
Featured Plugins
A selection of plugins curated by the Filament team
Custom Dashboards
Let your users build and share their own dashboards with a drag-and-drop interface. Define your data sources in PHP and let them do the rest.
Filament
Custom Fields
Eliminate custom field migrations forever. Let your users create and manage form fields directly in Filament admin panels with 20+ built-in field types, validation, and zero database changes.
Relaticle
Advanced Tables (formerly Filter Sets)
Supercharge your tables with powerful features like user-customizable views, quick filters, multi-column sorting, advanced table searching, convenient view management, and more. Compatible with Resource Panel Tables, Relation Managers, Table Widgets, and Table Builder!
Kenneth Sese