Filament date range table filter with presets
A reusable Filament 4 table filter with From and Until date pickers, quick presets, safe query building and readable filter chips.
FilamentTablesfilamentfiltersdate-rangetable
Code
<?php
namespace App\Filament\Tables;
use Carbon\Carbon;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
/** Usage: ->filters([DateRangeFilter::make('created_at')]) */
class DateRangeFilter
{
public static function make(string $column = 'created_at', ?string $label = null): Filter
{
$label ??= str($column)->replace('_', ' ')->title()->toString();
return Filter::make("{$column}_range")
->label($label)
->schema([
Select::make('preset')
->options(['today' => 'Today', '7d' => 'Last 7 days', '30d' => 'Last 30 days', 'month' => 'This month'])
->placeholder('Custom range')
->live()
// Picking a preset just fills the two pickers.
->afterStateUpdated(function (?string $state, Set $set) {
[$from, $until] = match ($state) {
'today' => [today(), today()],
'7d' => [today()->subDays(6), today()],
'30d' => [today()->subDays(29), today()],
'month' => [today()->startOfMonth(), today()],
default => [null, null],
};
$set('from', $from?->toDateString());
$set('until', $until?->toDateString());
}),
DatePicker::make('from')->native(false)->maxDate(fn ($get) => $get('until') ?: now()),
DatePicker::make('until')->native(false)->minDate(fn ($get) => $get('from')),
])
->query(fn (Builder $query, array $data): Builder => $query
// Dates are inclusive: use whole-day bounds, not the bare date.
->when($data['from'] ?? null, fn (Builder $q, string $d) => $q->where($column, '>=', Carbon::parse($d)->startOfDay()))
->when($data['until'] ?? null, fn (Builder $q, string $d) => $q->where($column, '<=', Carbon::parse($d)->endOfDay())))
// The chips above the table that let users remove one side.
->indicateUsing(function (array $data) use ($label): array {
$chips = [];
if ($data['from'] ?? null) {
$chips[] = "$label from ".Carbon::parse($data['from'])->toFormattedDateString();
}
if ($data['until'] ?? null) {
$chips[] = "$label until ".Carbon::parse($data['until'])->toFormattedDateString();
}
return $chips;
});
}
}
Needs: filament/filament@^4.0
How it works
What it solves
"Show me last month" is the most common request on any list of orders, events or payments. Filament's stock date filter handles one date; a range with presets and correct end-of-day behaviour needs a small custom filter, which this class packages for reuse on any column.
How it works
DateRangeFilter::make('created_at') returns a Filter with a three-field schema. A preset Select is live(); when changed, afterStateUpdated calculates the matching dates and writes them into the From and Until pickers with Set, so presets and manual dates share one code path. The pickers constrain each other: From cannot be after Until, and Until cannot be before From.
The query() closure uses when() so empty fields add no constraint. The bounds are converted to startOfDay() and endOfDay(), which matters for timestamp columns, comparing against a bare date would drop everything that happened on the last day.
indicateUsing() returns a short chip per side, such as "Created At from Mar 1, 2026". Filament shows these above the table and removes the matching filter value when a chip is dismissed.
Adapting it
Pass any column name and optionally a label, so one class serves paid_at, starts_at and so on. Add presets by extending the match. For a per-user timezone, parse the strings with that timezone before applying the bounds.
Pitfalls
- The preset field is stored in the filter state; the query ignores it, only the dates count.
- Comparisons on a function of the column (like
date(created_at)) skip indexes, keep the raw column. - Remember that dates are interpreted in the app timezone.
How to use it
- Copy the class to
app/Filament/Tables/. - Add it to a table:
->filters([DateRangeFilter::make('paid_at', 'Paid')]). - Make sure the column is a date or timestamp.
- Add
->deferFilters(false)on the table if you want results to update instantly. - Extend the preset list to fit your reporting periods.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.