Kanban by asign
CommunityDrag-and-drop Kanban board with transition modals, filters and policies — the Filament 5 successor of mokhosh/filament-kanban.
Author:
asign
Package health
Automated checks of this plugin's Composer package
15 checks
- Passed: GitHub Actions pinned to SHA
- Skipped: GitLab CI includes pinned to SHA
- Passed: Open security advisories
- Passed: Dependabot PR responsiveness — No open Dependabot PRs.
- Skipped: Renovate MR responsiveness
- Passed: Dependabot or Renovate configured
- Passed: Dependency update cooldown configured
- Passed: Provides a security policy
- Passed: Abandoned or archived — No consulted source marks the package abandoned (packagist, github).
- Passed: Commit and release recency — Active: last commit 2 days ago; no release date available.
-
Passed:
composer.lock not committed by library
—
composer.lockis absent from the released dist archive. - Passed: Dist archive is lean
-
Passed:
Current Laravel version supported
—
Package dependencies resolve together with current Laravel
13.0. -
Passed:
Current PHP version supported
—
Constraint
^8.3supports current PHP8.5. - Skipped: Current Symfony version supported
filament/
namespace. Review the source and install at your own risk. Found
malware or an unresolved security issue the author won't
address?
Report it
.
Documentation
- Free or Pro?
- Screenshots
- Requirements
- Installation
- Usage
- Configuration
- Migrating from mokhosh/filament-kanban
- Gotchas
- Extending
- Translations
- AI agents
- Testing
- Changelog
- More plugins by asign
- License
A drag-and-drop Kanban board for Filament 5. Columns come from an enum or from database rows, cards are any Eloquent model
(int, UUID or ULID keys), and the order is saved the way a real team board needs it: cards hidden by a search or a filter
keep their places. Moving a card can ask a question first (a lost reason, a comment), is authorized through your policy,
and can be refused by your own rule. It is the successor of mokhosh/filament-kanban, which stopped at Filament 3, with the
same class names and hooks so a board moves over by swapping a namespace.
No JavaScript to build, no Tailwind safelist: drag and drop uses the SortableJS Filament already ships, the stylesheet is
namespaced (.fi-kanban*) and uses Filament's own colour variables, so it follows your panel colours and dark mode.
#Free or Pro?
| Kanban (free, MIT) | Kanban Pro (€69 / €169) | |
|---|---|---|
| Board from an enum or from database rows, any Eloquent model (int / UUID / ULID) | ✓ | ✓ |
| Drag and drop that keeps the places of cards hidden by search or filters | ✓ | ✓ |
Ask before a move (modal fields), policy and canMove() authorization |
✓ | ✓ |
| Lazy columns: load more cards per column (opt-in) | ✓ | ✓ |
| Search box, foldable columns, edit modal, events, mokhosh-compatible API | ✓ | ✓ |
| Workflow designer: steps, transitions and automations edited in the panel, journal, webhooks | ✓ | |
| Transition rules: allow-list per step, required fields on entering a column | ✓ | |
| WIP limits, soft or hard | ✓ | |
| Column totals per currency, stale and overdue badges | ✓ | |
| Several boards as tabs, each with its own scope and limits; Mine / All toggles | ✓ | |
| Swimlanes (by owner, priority, any attribute) | ✓ | |
| Any Filament table filter on the board | ✓ |
Kanban Pro is an add-on: it requires the free package and builds on its documented extension points, so a board upgrades without rewriting.
#Screenshots
A board with enum columns, a search box, a locked card with its badge and a folded "Done" column:

Moving a card can ask a question first (a lost reason, a comment) before it is saved:

Dark mode follows the panel:

#Requirements
- PHP 8.3+
- Laravel 12 or 13
- Filament 5
#Installation
composer require asignua/filament-kanban
php artisan filament:assets
php artisan filament-kanban:install does the second step too. The plugin class is optional - boards are ordinary pages:
use Asignua\FilamentKanban\KanbanPlugin;
$panel->plugin(KanbanPlugin::make());
#Usage
#1. An enum-driven board
use Asignua\FilamentKanban\Concerns\IsKanbanStatus;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum TaskStatus: string implements HasColor, HasIcon, HasLabel
{
use IsKanbanStatus;
case Todo = 'todo';
case Doing = 'doing';
case Done = 'done';
public function getLabel(): string { return ucfirst($this->value); }
public function getColor(): string { return match ($this) { self::Todo => 'gray', self::Doing => 'warning', self::Done => 'success' }; }
public function getIcon(): string { return 'heroicon-m-bolt'; }
}
HasLabel, HasColor and HasIcon are optional; without them the column title is the case value. Override
kanbanCases() to keep some cases off the board, getTitle() to change a title.
php artisan make:kanban TasksBoard --model="App\Models\Task" --enum="App\Enums\TaskStatus"
use Asignua\FilamentKanban\Pages\KanbanBoard;
class TasksBoard extends KanbanBoard
{
protected static string $model = Task::class;
protected static string $statusEnum = TaskStatus::class;
protected static string $recordStatusAttribute = 'status'; // default
protected static string $recordTitleAttribute = 'title'; // default
protected static ?string $recordSortAttribute = 'position'; // where the manual order is stored; null = not stored
}
Cast the status column to the enum on the model ('status' => TaskStatus::class); a plain string column works too.
#2. Columns from the database
Override statuses() and return arrays. id is the column id (what the browser sends), value is what is written to the
status attribute (defaults to id):
protected static string $recordStatusAttribute = 'stage_id';
protected function statuses(): Collection
{
return Stage::query()->orderBy('position')->get()->map(fn (Stage $stage): array => [
'id' => (string) $stage->id,
'title' => $stage->name,
'value' => $stage->id,
'color' => $stage->color, // optional: a Filament colour name
'icon' => null, // optional
'collapsed' => false, // optional
]);
}
#3. On a resource
class TasksKanban extends \Asignua\FilamentKanban\Pages\KanbanResourcePage
{
protected static string $resource = TaskResource::class;
protected static string $statusEnum = TaskStatus::class;
}
// TaskResource::getPages(): 'kanban' => TasksKanban::route('/kanban')
The model and the base query (tenancy, soft-delete scopes) come from the resource. Any other page class can use the trait
Asignua\FilamentKanban\Concerns\InteractsWithKanban instead.
#Order of cards
With $recordSortAttribute set, the board sorts by that column and saves the order after every drop. Only the visible
cards change places. Search "invoice", drag one match above another: the matches swap slots, the hidden cards keep the
slots they had. Writes use the query builder, so there are no model events and updated_at is not touched (a touch would
make every renumbered card flash).
Without it nothing is persisted (cards are shown by key). If the model uses spatie/eloquent-sortable, its ordered()
scope and setNewOrder() are used, like in mokhosh/filament-kanban.
#Search and filters
A debounced search box filters the cards by $recordTitleAttribute; set protected array $searchableAttributes = ['title', 'client.name']
(dotted = through a relation) or protected bool $searchable = false. The term is also in the URL (?search=).
For your own filters override modifyRecordsQuery(Builder $query): Builder. Do not put filters in getEloquentQuery():
that query also finds the whole column when positions are saved.
#Asking before a move
use Filament\Forms\Components\Textarea;
protected function columnTransitionSchema(string $from, string $to): ?array
{
return $to === 'lost' ? [Textarea::make('lost_reason')->required()] : null;
}
When the schema is not empty, dropping a card into that column opens a Filament modal instead of moving it. Confirm moves
the card and fills the form data onto the record (onTransitionConfirmed(), override it when the fields are not columns).
Need the answers in the same call that writes the move (e.g. one service method taking stage + lost reason)? Override onRecordTransitioned($recordId, $from, $to, $fromIds, $toIds, array $data) - it runs in the move's transaction, its default calls onStatusChanged() then onTransitionConfirmed(); or read currentTransitionData() inside onStatusChanged().
The card is not moved until you confirm: while the modal is open the board re-renders and shows the card in its original column, and Confirm moves it. Cancel just closes the modal; nothing changed. Need the record in the form? Override
transitionSchemaFor(Model $record, string $from, string $to) and transitionFormDefaults(...).
#Who may move what
canMove(Model $record, string $from, string $to): bool- default: the model's policyupdate, and allowed when the model has no policy (like a Filament resource). Denied: a danger notification, the card jumps back.$from === $tofor a reorder.validateMove(Model $record, string $from, string $to): ?string- return a message to refuse a move for a business reason.isRecordDraggable(Model $record): bool- lock a card (it cannot be picked up, and a forged request is refused too).canEditRecord(Model $record): bool- same for the edit modal. A board whose cards are links (recordUrl()) or that sets$disableEditModalexposes no edit action at all.- With
->strictAuthorization()on the panel, a model without a policy is refused (move and edit), like a resource. - A standalone board checks the model's
viewAnypolicy incanAccess()(override it for your own rule); for per-row visibility usemodifyRecordsQuery(). - A reorder is authorized for the dragged card only. Other cards of the column are renumbered around it, including locked ones, so a locked card can change its stored position (not its column) when a neighbour is dragged past it.
#Columns
All columns can be folded with the button in the header; a folded column is a narrow strip that still accepts drops. Fold
some at start with protected array $collapsedStatuses = ['done'] (or 'collapsed' => true in a status array). The choice is
remembered per browser. Turn the button off with protected bool $collapsibleColumns = false. An empty column shows a drop
hint. On a phone the columns scroll horizontally and snap.
#Lazy columns
A column with hundreds of cards (a CRM's "Won" and "Lost") does not have to load them all. Opt in with a page size:
protected ?int $recordsPerColumn = 25;
Each column then loads its first 25 cards and ends with a Show 25 more button. The header badge keeps showing the column's total (with the search applied), not the number on screen. To page only some columns, override the method (null = that column loads everything):
protected function recordsPerColumn(KanbanColumn $column): ?int
{
return in_array($column->id, ['won', 'lost'], true) ? 25 : null;
}
Good to know:
- Default is
null: every card is loaded, with exactly the old queries. - The cards of a lazy column come from the same query as before (
getEloquentQuery()> search >modifyRecordsQuery()), withwhere status = columnand alimit; the totals are one groupedcount(*). No window functions, so it runs on MySQL, MariaDB, PostgreSQL and SQLite. - Dragging works as with a search: cards that are not loaded keep their places. A card dropped into a lazy column is placed right behind the last loaded card (the column is renumbered in its stored order in a few bulk updates, so tied or empty positions are fine) and the column's page grows by one, so nothing that was on screen disappears. Changing the search starts the pages again from the first one.
- Overriding
records()switches lazy columns off: that method returns every card at once, so there is nothing to cut a page from. UsemodifyRecordsQuery()/recordRelations()for narrowing instead. - An add-on that sums or counts a column must use
$column->total()/columnRecordsQuery($column), not the loaded$column->records.
#Cards
By default a card shows the title; clicking it opens the edit modal. Override:
protected function recordTitle(Model $record): string; // default: the title attribute
protected function recordDescription(Model $record): ?string; // grey line under the title
protected function recordBadges(Model $record, KanbanColumn $column): array; // [['label' => 'Overdue', 'color' => 'danger', 'icon' => 'heroicon-m-clock']]
protected function recordExtras(Model $record, KanbanColumn $column): array; // free markup (Htmlable)
protected function recordUrl(Model $record): ?string; // the card becomes a link, no modal
protected function recordRelations(): array; // eager loads
A card that was updated in the last $flashSeconds (default 3) pulses once.
#Edit modal
protected function getEditModalFormSchema(int|string|null $recordId): array
{
return [TextInput::make('title')->required(), Textarea::make('notes')];
}
protected function editRecord(int|string $recordId, array $data, array $state): void
{
Task::find($recordId)?->update($data); // default: forceFill($data)->save()
}
protected string $editModalTitle = 'Edit task';
protected string $editModalWidth = '2xl';
protected bool $editModalSlideOver = true;
protected string $editModalSaveButtonLabel = 'Save';
protected string $editModalCancelButtonLabel = 'Cancel';
public bool $disableEditModal = false;
#Views
Publish with php artisan vendor:publish --tag=filament-kanban-views, or point one board at its own view:
protected static string $toolbarView = 'my-board.toolbar';
protected static string $headerView = 'my-board.header'; // a column header
protected static string $statusView = 'my-board.column';
protected static string $recordView = 'my-board.card';
protected static string $scriptsView = 'my-board.scripts'; // empty by default
protected string $view = 'my-board.page';
The column view receives $column (a read-only KanbanColumn: id, title, value, color, icon, records, meta;
also readable as $column['title']), the card view $record and $column.
#Events
Asignua\FilamentKanban\Events\KanbanRecordMoved (record, from, to, board, data) is dispatched after a card changed
column, and onRecordMoved() is called on the board. Reorders inside a column do not fire it.
#Configuration
There is no config file: everything is a property or an overridable method on the board (see above and
docs/SPEC.md). The translations are published with --tag=filament-kanban-translations.
#Migrating from mokhosh/filament-kanban
mokhosh/filament-kanban stops at Filament 3 (its issue "Support filament v4" is still open). This package keeps its class
names, properties and hook signatures on purpose, so a board moves over by changing a namespace and fixing what Filament 4/5
itself changed. A test (MokhoshMigrationTest) runs a board written exactly as their README shows it - public hooks,
a hand-built records() and statuses() - against this package.
composer remove mokhosh/filament-kanban && composer require asignua/filament-kanban.- Swap the namespaces:
Mokhosh\FilamentKanban\Pages\KanbanBoard->Asignua\FilamentKanban\Pages\KanbanBoard,Mokhosh\FilamentKanban\Concerns\IsKanbanStatus->Asignua\FilamentKanban\Concerns\IsKanbanStatus. - Fix the Filament 4/5 differences in your board class (these are Filament's, not ours):
protected static string|BackedEnum|null $navigationIcon,protected string $viewis no longer static, form components come fromFilament\Forms\Components, layout fromFilament\Schemas\Components. - Run
php artisan filament:assets(orfilament-kanban:install, the same command name) and delete the old published assets. - Delete a published
kanban-*view unless you changed it on purpose; the old markup uses Tailwind classes your panel theme does not compile.
#API mapping
| mokhosh/filament-kanban | filament-kanban | Notes |
|---|---|---|
Mokhosh\FilamentKanban\Pages\KanbanBoard |
Asignua\FilamentKanban\Pages\KanbanBoard |
Also Pages\KanbanResourcePage for a page on a resource, or the trait Concerns\InteractsWithKanban on any page. |
protected static string $model |
same | |
protected static string $statusEnum + IsKanbanStatus on the enum |
same | kanbanCases() and getTitle() work as before; HasLabel / HasColor / HasIcon are honoured too. An enum's own static statuses() is still read. |
protected static string $recordTitleAttribute / $recordStatusAttribute |
same | |
statuses(): Collection of ['id' => ..., 'title' => ...] |
same | Extra keys: value, color, icon, collapsed, meta. id is always a string. |
records(): Collection |
same, but prefer modifyRecordsQuery(Builder) |
Overriding records() replaces search, ordering and lazy columns. |
getEloquentQuery() |
same | Keep UI filters out of it (they belong in modifyRecordsQuery()): it also finds the whole column when positions are saved. |
onStatusChanged($recordId, $status, $fromOrderedIds, $toOrderedIds) |
same signature, protected | Browser entry point is statusChanged(), which authorizes first. A public override still works but is callable from the browser. |
onSortChanged($recordId, $status, $orderedIds) |
same signature, protected | Entry point sortChanged(). |
Livewire events status-changed / sort-changed |
same | |
Spatie eloquent-sortable (ordered(), setNewOrder()) |
still works when no $recordSortAttribute is set |
Or name a column: $recordSortAttribute = 'position'; only the visible cards change places. |
$disableEditModal |
same | A card with recordUrl() links instead of opening the modal. |
getEditModalFormSchema($recordId) |
same | Now feeds a Filament action modal (kanbanEdit), so the schema is an ordinary action schema. |
editRecord($recordId, $data, $state) |
same | |
$editModalTitle, $editModalWidth, $editModalSlideOver, $editModalSaveButtonLabel, $editModalCancelButtonLabel |
same | |
$headerView, $recordView, $statusView, $scriptsView, $view |
same | New: $toolbarView. Views still read $statuses / $status['records'] and $record as a Model. |
make:kanban, filament-kanban:install, tag filament-kanban-views |
same | |
HasRecentUpdateIndication (deprecated) |
built in | The flash is automatic ($flashSeconds). |
TestsFilamentKanban testing macros |
not ported | Test with Livewire::test(Board::class)->call('statusChanged', ...). |
| - | canMove(), validateMove(), isRecordDraggable(), canEditRecord() |
New: authorization through the model policy and your own rules. |
| - | columnTransitionSchema() / onRecordTransitioned() |
New: ask a question (lost reason) before a move. |
| - | $recordsPerColumn |
New: lazy columns with "Show more" for columns with hundreds of cards. |
| - | recordBadges(), recordExtras(), recordDescription(), recordUrl(), recordRelations(), search, folding |
New. |
There is no compatibility trait: the names are already the same, so a shim would only add a second way to write the same board.
What differs, in short:
- The hooks are protected. In mokhosh
onStatusChanged()is public, which means any user can call it from the browser console and skip your checks. Here the browser callsstatusChanged()/sortChanged(), which authorize and validate, and then call the (protected) hooks. If you override a hook aspublic, you expose it again. - The order is saved with a column you name (
$recordSortAttribute), visible-only.spatie/eloquent-sortablemodels still work when no sort attribute is set. - A move is checked against the model's policy (
update) - a model with a policy that deniesupdatecan no longer be moved. getEditModalFormSchema()now feeds a Filament action modal (kanbanEdit), so the form is an ordinary action schema.- New: transition modal,
canMove()/validateMove()/isRecordDraggable(), search, collapsible columns,recordUrl(), badges and slots, ULID/UUID keys, the resource page. - Not ported: the deprecated
HasRecentUpdateIndicationtrait (the flash is built in) and theTestsFilamentKanbantesting macros.
#Gotchas
- A filter in
getEloquentQuery()corrupts the order. The board finds the whole column through that query when it saves positions. A hidden card then does not exist for it, and its position collides with a visible one. Filters go tomodifyRecordsQuery(). - Public hooks are callable from the browser. Keep
on*hooks protected, authorize incanMove(). - Overriding
records()replaces the search and the ordering. UsemodifyRecordsQuery()unless you build the collection by hand. statuses()ids are strings. A numeric key is compared as a string; put the real value invalueso the attribute gets an int.- Run
php artisan filament:assetsafter install and after an update. The board's script and stylesheet are Filament assets; a stale published copy gives a board that renders but does not drag. - Column cards come from one query. A column with thousands of cards is slow; narrow
modifyRecordsQuery()(for example by date). - Search uses
LIKE %term%;%and_typed by the user act as wildcards.
#Extending
Slots for toolbar, column headers, card badges and footers, column meta, query modification and before-move validation are
documented in docs/EXTENDING.md. The paid add-on asignua/filament-kanban-pro (workflow designer, transition rules, WIP limits,
column totals, swimlanes, stale badges, boards as tabs, table-filter integration) is built only on these.
#Translations
The interface ships in English, Ukrainian, German, Spanish, French, Italian, Dutch, Polish, Brazilian Portuguese and Turkish
under the filament-kanban::filament-kanban namespace. A test keeps every language in step with the English keys. Override a
string by publishing the translations (--tag=filament-kanban-translations) and editing the copy in
lang/vendor/filament-kanban.
#AI agents
The package ships Laravel Boost guidelines (resources/boost/guidelines/core.blade.php) so a
coding agent wires it up correctly.
#Testing
composer install
vendor/bin/phpunit
vendor/bin/phpstan analyse --memory-limit=1G
vendor/bin/pint --test
Try the demo boards by hand: vendor/bin/testbench workbench:build, then vendor/bin/testbench serve --host=0.0.0.0,
log in at /admin as [email protected] / password.
#Changelog
See CHANGELOG.md.
#More plugins by asign
- Kanban Pro — workflow designer, transition rules, WIP limits and totals for boards (swimlanes, several boards as tabs)
- Record Lock — stop two people editing the same record
- Themes — per-user and per-panel colour themes
- Activity Log Plus — audit trail and history for models
All plugins: https://filamentphp.com/plugins?q=asign
#License
The MIT License (MIT). See LICENSE.md.
The author
asign is a small web-dev company from Lviv, Ukraine. We build business applications on Laravel and Filament — CRMs, automation systems for standard and non-standard business processes, booking and content management systems, including our own Filament-based CMS. We open-source the parts that prove useful beyond a single project
From the same author
VAT ID
Tax ID and VAT number validation for Filament: offline format and checksum rules for EU VAT, PL NIP/REGON, CZ IČO/DIČ and UA ЄДРПОУ/РНОКПП, plus an input that looks companies up in VIES, ARES, the Polish white list and GUS.
Author:
asign
XLSX Export
Download the current table as a real Excel file: filters, search and sort applied, typed numbers and dates, streamed instantly. No queue, no CSV.
Author:
asign
Rich Editor Toolkit
Ten free RichEditor plugins: Word/Docs paste cleanup, image alt, captions and alignment, link rel options, fullscreen, HTML source view, and length limits.
Author:
asign
Clusters
Group several fields into one input row with a shared label, joined borders and one focus ring — the successor of guava/filament-clusters.
Author:
asign
Featured Plugins
A selection of plugins curated by the Filament team
Noir Theme
A theme that gives panels a focused, refined look with near-black surfaces, crisp actions, and restrained color.
Filament
Soft Theme
A theme that gives panels a warm, approachable look with rounded shapes, gentle colors, and serif headings.
Filament
Spotlight Pro
Browse your Filament Panel with ease. Filament Spotlight Pro adds a Spotlight like Command Palette to your Filament Panel.
Dennis Koch