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
Modern board additions
These settings apply to Modern boards only. A classic board ignores them.
| Method | Purpose |
|---|---|
createCardAction(Action|Closure|null) | The action behind the header + and the add row |
columnSummary(?Closure) | Header text per column, from the filtered column query |
groupings(array|Closure) | "Group by" options, each a Grouping |
lanesPerPage(int) | Lanes rendered before "Show more lanes" (default 10) |
Column::limit(int|Closure|null) | Hard limit per column |
authorizeCardMove() / persistCardMove() | Page hooks for checks and custom writes |
See Customization for the add card action, card components, summaries and limits. See Swimlanes for groupings and lanes, and Design System for styling.
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()
->limit(3) // Modern boards: hard limit, refuses drops when full
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)
On a Modern board, moveCard() takes two more arguments and returns bool. It returns false when it refuses a move.
$this->moveCard(
string $cardId,
string $targetColumnId,
?string $afterCardId = null,
?string $beforeCardId = null,
?string $fromLaneId = null,
?string $toLaneId = null,
): bool
Override authorizeCardMove() and persistCardMove() instead of moveCard(). See Custom Card Movement Logic.
Pagination Control
// Load more cards for pagination
$this->loadMoreItems(string $columnId, ?int $count = null)
// Modern boards: load more cards into one lane cell
$this->loadMoreItems(string $columnId, ?int $count = null, ?string $laneId = 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}`);
});
});