Installation
Requirements
- PHP 8.3 or newer
- Laravel 12 or 13
- Filament 4 or 5
1. Require the package
composer require packstub/filament-kanban
php artisan filament:assets
There is no config file and there are no migrations. The board uses the SortableJS that Filament already ships and a small stylesheet built on your panel's colour variables, so there is nothing to add to your theme. The script and the stylesheet load only on pages that show a board.
Run php artisan filament:assets again after every update of the package: Filament copies the assets into public/.
2. Add a board page
A board lives on a page. KanbanResourcePage sits next to a resource's list; KanbanPage is a standalone panel page. Both ask for one method, kanban():
namespace App\Filament\Resources\Tasks\Pages;
use App\Filament\Resources\Tasks\TaskResource;
use App\Models\Task;
use Packstub\Kanban\Board;
use Packstub\Kanban\Card;
use Packstub\Kanban\Column;
use Packstub\Kanban\Pages\KanbanResourcePage;
class TaskBoard extends KanbanResourcePage
{
protected static string $resource = TaskResource::class;
public function kanban(Board $board): Board
{
return $board
->query(fn () => Task::query())
->columnAttribute('status')
->columns([
Column::make('todo')->label('To do')->color('sky'),
Column::make('doing')->color('amber'),
Column::make('done')->color('emerald'),
])
->card(fn (Task $task) => Card::make()
->eyebrow($task->key)
->title($task->title)
->url(TaskResource::getUrl('edit', ['record' => $task])));
}
}
Register it in the resource like any other page:
public static function getPages(): array
{
return [
'index' => ListTasks::route('/'),
'board' => TaskBoard::route('/board'),
];
}
A standalone KanbanPage is discovered with your other pages. It is a normal Filament page, so getHeaderActions(), navigation and authorization work as usual.
3. The query decides what is on the board
query() is the single source of records. Every read (cards, counts, search, summaries) and every write (moves, card actions) goes through it, so scope it there: a tenant, a team, the current user's projects, a date window.
->query(fn () => Task::query()->whereBelongsTo(Filament::getTenant())->with('assignee'))
A card that falls out of the query (someone else archived it, the user lost access) can no longer be moved or acted on: the server answers "This card is no longer on the board".
A board in any Livewire component
The pages are thin: the board itself lives in the InteractsWithKanban trait. Put it on any Livewire component and include the view:
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Livewire\Component;
use Packstub\Kanban\Board;
use Packstub\Kanban\Concerns\InteractsWithKanban;
class ProjectBoard extends Component implements HasActions, HasSchemas
{
use InteractsWithActions;
use InteractsWithKanban;
use InteractsWithSchemas;
public Project $project;
public function kanban(Board $board): Board
{
return $board->query(fn () => $this->project->tasks()->getQuery())->key('project-board');
// …
}
public function render()
{
return view('livewire.project-board'); // @include('packstub-kanban::board') + <x-filament-actions::modals />
}
}
HasActions and HasSchemas are only needed for card and create actions.