Kanban plugin screenshot
Dark mode ready
Multilingual support
Supports v5.x

Kanban by asign

Community

Drag-and-drop Kanban board with transition modals, filters and policies — the Filament 5 successor of mokhosh/filament-kanban.

Tags: Panels Widget Form Layout
Supported versions:
5.x
asign avatar Author: asign

Package health

Automated checks of this plugin's Composer package

100 / 100
Security 100
Maintenance 100
Ecosystem 100
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.lock is 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.3 supports current PHP 8.5.
  • Skipped: Current Symfony version supported
Third-party plugin. This is built by the community, not the Filament team. Filament does not review, endorse, or vet the security of plugins outside the 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 .
Powered by Plumb Last scanned 5 hours ago

Documentation

Stand With Ukraine

Filament Kanban

Tests

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:

A Kanban board with three columns, a locked card and a collapsed Done column

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

The transition modal asking for a reason when a card moves to Done

Dark mode follows the panel:

The same board in dark mode

#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 policy update, and allowed when the model has no policy (like a Filament resource). Denied: a danger notification, the card jumps back. $from === $to for 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 $disableEditModal exposes 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 viewAny policy in canAccess() (override it for your own rule); for per-row visibility use modifyRecordsQuery().
  • 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()), with where status = column and a limit; the totals are one grouped count(*). 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. Use modifyRecordsQuery() / 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.

  1. composer remove mokhosh/filament-kanban && composer require asignua/filament-kanban.
  2. Swap the namespaces: Mokhosh\FilamentKanban\Pages\KanbanBoard -> Asignua\FilamentKanban\Pages\KanbanBoard, Mokhosh\FilamentKanban\Concerns\IsKanbanStatus -> Asignua\FilamentKanban\Concerns\IsKanbanStatus.
  3. Fix the Filament 4/5 differences in your board class (these are Filament's, not ours): protected static string|BackedEnum|null $navigationIcon, protected string $view is no longer static, form components come from Filament\Forms\Components, layout from Filament\Schemas\Components.
  4. Run php artisan filament:assets (or filament-kanban:install, the same command name) and delete the old published assets.
  5. 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 calls statusChanged() / sortChanged(), which authorize and validate, and then call the (protected) hooks. If you override a hook as public, you expose it again.
  • The order is saved with a column you name ($recordSortAttribute), visible-only. spatie/eloquent-sortable models still work when no sort attribute is set.
  • A move is checked against the model's policy (update) - a model with a policy that denies update can 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 HasRecentUpdateIndication trait (the flash is built in) and the TestsFilamentKanban testing 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 to modifyRecordsQuery().
  • Public hooks are callable from the browser. Keep on* hooks protected, authorize in canMove().
  • Overriding records() replaces the search and the ordering. Use modifyRecordsQuery() unless you build the collection by hand.
  • statuses() ids are strings. A numeric key is compared as a string; put the real value in value so the attribute gets an int.
  • Run php artisan filament:assets after 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 avatar Author: asign

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

Plugins
33
Stars
29

From the same author