API Reference
Board Configuration
| Method | Description | Required |
|---|---|---|
query(Builder) | Set the Eloquent query as data source | Yes |
columnIdentifier(string) | Field name for column status | Yes |
positionIdentifier(string) | Field name for drag-drop ordering | Yes |
columns(array) | Define board columns | Yes |
recordTitleAttribute(string) | Field name for card titles | |
cardLabel(string|Closure|null) | Name of a single card, e.g. "Task" | |
pluralCardLabel(string|Closure|null) | Name of a set of cards, e.g. "Tasks" | |
cardSchema(Closure) | Rich card content using Schema | |
cardActions(array) | Actions available on each card | |
columnActions(array) | Actions available on column headers | |
searchable(array) | Enable search on specified fields | |
filters(array) | Add filtering capabilities | |
cardAction(string) | Make cards clickable with action | |
cardsPerColumn(int) | Cards to load per column (pagination) | |
filtersFormWidth(Width) | Filter panel width | |
filtersFormColumns(int) | Columns in filter form | |
filtersLayout(FiltersLayout) | Filter display layout (Dropdown, AboveContent, etc.) | |
headerToolbar(bool) | Render filters/search inline with page title | |
collapseEmptyColumns(bool) | Collapse columns holding no cards into a rail |
Board Methods
Essential Configuration
public function board(Board $board): Board
{
return $board
->query(Task::query()) // Required: Data source
->columnIdentifier('status') // Required: Column field
->positionIdentifier('position') // Required: Position field
->columns([ // Required: Column definitions
Column::make('todo')->label('To Do')->color('gray'),
Column::make('done')->label('Done')->color('green'),
]);
}
Content Configuration
->recordTitleAttribute('title') // Card title field
->cardLabel('Task') // Defaults to the model label
->pluralCardLabel('Tasks') // Defaults to the plural model label
->cardSchema(fn(Schema $schema) => $schema // Rich card content
->components([
TextEntry::make('description'),
TextEntry::make('due_date')->date(),
])
)
Card Labels
Explicit labels take precedence over resource and model labels. Otherwise, Flowforge uses the board page's resource, then the model's resource in the current panel, then the model name. Without a resource or model, it uses the package translations.
Model names are not translated automatically. Existing boards, including standalone boards, can therefore display different labels after upgrading. Set translated labels explicitly to preserve the previous wording:
->cardLabel(fn (): string => __('flowforge::flowforge.card_label'))
->pluralCardLabel(fn (): string => __('flowforge::flowforge.plural_card_label'))
Use your application's translation keys for custom names. Set both labels for languages whose plural forms cannot be derived from the singular label. Closures resolve in the current locale when the board renders.
Actions Configuration
->cardActions([ // Card-level actions
EditAction::make()->model(Task::class),
DeleteAction::make()->model(Task::class),
])
->columnActions([ // Column-level actions
CreateAction::make()->model(Task::class),
])
->cardAction('edit') // Make cards clickable
cardAction() takes the name of an action registered in cardActions(). When that
action is configured with ->url(), the card renders as a native link (respecting
->openUrlInNewTab()) instead of opening a modal, so middle-click and "copy link
address" work as expected:
->cardActions([
Action::make('view')
->url(fn (Task $record): string => TaskResource::getUrl('view', ['record' => $record])),
])
->cardAction('view')
A url decides, exactly as it does for a table row. Filament's ListRecords builds
recordUrl() from the first view/edit action whose getUrl() is filled and has
recordAction() skip those same actions, without consulting their modal state. A card
follows that rule, so the same action behaves the same way in a board and in a table.
Two consequences worth knowing:
- An action declaring both a url and a modal (
->requiresConfirmation(), a custom modal heading, a schema) renders as a link and never opens that modal. Filament's own table row and actions dropdown do the same. Pick one or the other. - A url inherited from the page's
getDefaultActionUrl()counts, sincegetUrl()consults it. Only->postToUrl()actions stay on the click handler, because a POST needs a form rather than an anchor.
Search & Filtering
use Filament\Tables\Enums\FiltersLayout;
use Filament\Support\Enums\Width;
->searchable(['title', 'description']) // Enable search
->filters([ // Add filters
SelectFilter::make('priority'),
Filter::make('overdue')->query(fn($q) =>
$q->where('due_date', '<', now())
),
])
->filtersLayout(FiltersLayout::AboveContent) // Display filters above board
->filtersFormWidth(Width::Large) // Filter panel width
->filtersFormColumns(3) // Columns in filter form
->headerToolbar() // Inline filters/search in page header
->collapseEmptyColumns() // Collapse columns holding no cards
Column Configuration
Column::make('identifier')
->label('Display Name') // Column header text
->color('blue') // Column color theme
->icon('heroicon-o-flag') // Column header icon
->hidden(fn () => ! auth()->user()?->isStaff()) // Hide from rendering
->visible(fn () => $this->showArchived) // Inverse of hidden()
Building a column from a backed enum
Column::enum(TaskStatus::Todo) // identifier = $enum->value
When the enum implements HasLabel, HasColor, or HasIcon those values are
applied automatically; missing contracts are skipped. See the
Column Configuration guide
for a full example.
Conditional visibility
Hidden columns are excluded from all column getters (getColumns(),
getColumnIdentifiers(), getColumnLabels(), getColumnColors()) — see
Conditional Visibility
for a worked example.
Available Colors
gray- Neutral/defaultblue- Primary/in-progressgreen- Success/completedred- Error/urgentamber- Warning/reviewpurple- Custom statuspink- Custom status
Livewire Methods
These methods are available in your board components for programmatic control:
Card Management
// Move card between columns
$this->moveCard(string $cardId, string $targetColumnId, ?string $afterCardId = null, ?string $beforeCardId = null)
Pagination Control
// Load more cards for pagination
$this->loadMoreItems(string $columnId, ?int $count = null)
// Load all cards to enable reordering
$this->loadAllItems(string $columnId)
// Check if all cards are loaded
$this->isColumnFullyLoaded(string $columnId): bool
// Get position for new card in column
$this->getBoardPositionInColumn(string $columnId): string
Performance Features
- Intelligent Pagination: Efficiently handles 100+ cards per column
- Infinite Scroll: Smooth loading with 80% scroll threshold
- Optimistic UI: Immediate feedback with rollback on errors
- Fractional Ranking: Prevents database locks during reordering
- Query Optimization: Cursor-based pagination with eager loading
Livewire Events
Flowforge dispatches these events for frontend integration and custom logic:
| Event | Payload | When Fired |
|---|---|---|
kanban-card-moved | cardId, columnId, position | After a card is moved |
kanban-items-loaded | columnId, loadedCount, totalCount, isFullyLoaded | After pagination loads more cards |
kanban-all-items-loaded | columnId, totalCount | After "Load All" completes |
Listening to Events
use Livewire\Attributes\On;
#[On('kanban-card-moved')]
public function onCardMoved(string $cardId, string $columnId, string $position): void
{
// Custom logic when a card moves (e.g., send notification, update analytics)
}
#[On('kanban-items-loaded')]
public function onItemsLoaded(string $columnId, int $loadedCount, int $totalCount, bool $isFullyLoaded): void
{
// Track loading progress
}
JavaScript Listeners
document.addEventListener('livewire:init', () => {
Livewire.on('kanban-card-moved', ({ cardId, columnId }) => {
console.log(`Card ${cardId} moved to ${columnId}`);
});
});