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' => 'Shipped',
'delivered' => '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 to avoid boot order conflicts:
class CustomTablePage extends Page
{
use InteractsWithTable;
use HasHeaderFilters;
protected static string $table = 'orders';
}
CSS Conflicts:
Filter State Persistence (v2.0.4 Fix):
->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 where Livewire trait boot order differed.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:
->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:
public function query(Builder $query, array $data): Builder
{
FilterJob::dispatch($query, $data);
return $query; // Return early or use a placeholder
}
Custom Table Pages (v2.0.4 Compatibility): When creating custom table pages, ensure proper trait ordering:
class CustomOrdersTable extends Table
{
protected static string $model = Order::class;
// Ensure HasHeaderFilters is loaded after InteractsWithTable
use \Leek\FilamentHeaderFilters\Concerns\HasHeaderFilters;
}
How can I help you explore Laravel packages today?