leek/filament-header-filters
Add inline filters to Filament table column headers. Attach any BaseFilter (selects, date pickers, min/max ranges, custom schemas) as a richer alternative to individual searchable fields. Works with Filament v4/v5, PHP 8.2+.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require leek/filament-header-filters
Add the HasHeaderFilters trait to your Filament resource's ListRecords page or custom table page:
use Leek\FilamentHeaderFilters\Concerns\HasHeaderFilters;
class OrderResource extends Resource
{
public static function getPages(): array
{
return [
'index' => Pages\ListOrders::route('/'),
];
}
}
class ListOrders extends ListRecords
{
use HasHeaderFilters;
}
Add CSS:
Update your Vite theme file (e.g., resources/css/filament/filament.css) to include:
@import '../../../../vendor/filament/filament/resources/css/theme.css';
@import '../../../../vendor/leek/filament-header-filters/resources/css/filament-header-filters.css';
Rebuild assets:
npm run dev
First Use Case:
Attach a SelectFilter to a column in your table definition:
Table::make([
TextColumn::make('status')
->headerFilter(
SelectFilter::make('status')
->options([
'pending' => 'Pending',
'shipped' => 'Delivered',
])
),
]);
Basic Filtering:
Use built-in Filament filters (SelectFilter, DatePickerFilter, RangeFilter) directly in column headers:
TextColumn::make('created_at')
->headerFilter(
DatePickerFilter::make('created_at')
->native(false)
),
Custom Filter Logic:
Extend BaseFilter for complex logic (e.g., multi-field validation):
class CustomStatusFilter extends BaseFilter
{
protected string $name = 'custom_status_filter';
public function buildSchema(): array
{
return [
Select::make('status')
->options(['active', 'inactive'])
->required(),
Toggle::make('include_archived'),
];
}
public function query(Builder $query, array $data): Builder
{
return $query->where('status', $data['status'])
->when($data['include_archived'], fn($q) => $q->orWhere('archived_at', '!=', null));
}
}
Attach it to a column:
TextColumn::make('status')
->headerFilter(CustomStatusFilter::make()),
Dynamic Options: Fetch filter options dynamically (e.g., from a database):
TextColumn::make('category')
->headerFilter(
SelectFilter::make('category')
->options(fn() => Category::query()->pluck('name', 'id'))
),
Conditional Filters: Show/hide filters based on user roles or column visibility:
TextColumn::make('price')
->visible(fn() => auth()->user()->can('view_prices'))
->headerFilter(
RangeFilter::make('price')
->visible(fn() => auth()->user()->can('filter_prices'))
),
Reusing Filters Across Columns:
Define filters in a resource's getHeaderFilters() method and reuse them:
public static function getHeaderFilters(): array
{
return [
'status' => SelectFilter::make('status')
->options(['pending', 'shipped']),
];
}
// In table definition:
TextColumn::make('status')->headerFilter('status'),
Custom Table Pages:
For custom table pages (e.g., extending Table), ensure the HasHeaderFilters trait is added after the InteractsWithTable trait:
class CustomTablePage extends Page
{
use InteractsWithTable;
use HasHeaderFilters;
protected static string $table = 'orders';
}
Handling Table Rebuilds (v2.0.8 Fix):
With v2.0.8, header filters now persist across table rebuilds (e.g., after resetTable() calls). No manual workarounds are needed:
// Previously required workaround (no longer needed):
// $this->table->resetTable();
// $this->table->bootedInteractsWithTable();
CSS Conflicts:
Filter State Persistence (v2.0.8 Fix):
resetTable() calls (fixed in v2.0.8). Remove any custom workarounds.->persistUsing() to ensure proper state persistence:
SelectFilter::make('status')
->persistUsing(fn($state) => "status_{$state}"),
Query Scope Conflicts:
headerFilter() with modifyQueryUsing() or getTableFilters() for the same column, as they may override each other.Livewire Component Isolation:
HasHeaderFilters trait is only added to components that extend InteractsWithTable (e.g., ListRecords or custom table pages). Adding it to unrelated components will cause errors.HasTable pages.Filter Validation:
public function rules(): array
{
return [
'status' => ['required', Rule::in(['pending', 'shipped'])],
];
}
Multi-Select to Single-Select Migration:
MultiSelectFilter to SelectFilter, clear any stale filter state in your database or session to avoid issues with normalized single-select values.Filter Not Appearing:
->headerFilter() called.HasHeaderFilters is properly registered in the resource and extends InteractsWithTable.Filter Not Working After Table Rebuild (v2.0.8):
resetTable(), ensure you’re using v2.0.8+. The fix resolves this by tracking filters per Table instance via WeakMap.Filter Not Working:
->log() to custom filters to debug query modifications:
public function query(Builder $query, array $data): Builder
{
\Log::info('Filter data:', $data);
return $query->where(...);
}
Performance Issues:
->native(true) for DatePickerFilter to leverage browser-native pickers (reduces payload size).->searchable() in SelectFilter:
SelectFilter::make('category')
->options(Category::query()->pluck('name', 'id'))
->searchable(),
Custom Filter Components:
Extend BaseFilter to create reusable filter types (e.g., MultiSelectHeaderFilter):
class MultiSelectHeaderFilter extends BaseFilter
{
protected string $name = 'multi_select';
public function buildSchema(): array
{
return [
MultiSelect::make('tags')
->options(['tag1', 'tag2']),
];
}
// ... implement query logic
}
Override Default Styling: Target the package's Blade components in your CSS:
/* Target the filter dropdown container */
.filament-header-filters-dropdown {
width: 200px;
}
/* Style the filter icon */
.filament-header-filters-icon {
color: #3b82f6;
}
Add Icons: Use Filament's icon system to customize filter icons:
SelectFilter::make('status')
->icon('heroicon-o-check-circle'),
Internationalization:
Extend the package's language lines in resources/lang/vendor/filament-header-filters:
return [
'filters' => [
'select_placeholder' => 'Choose an option...',
],
];
Server-Side Processing: For heavy filters, offload processing to a queue:
How can I help you explore Laravel packages today?