codewithkyrian/filament-date-range
Adds a date range picker field to Filament forms, letting users select start and end dates through a clean, user-friendly interface. It integrates smoothly with Filament panels and Laravel apps for quick date-range input and filtering.
Install and register
composer require codewithkyrian/filament-date-range
The service provider is auto-discovered; the package registers its Alpine/Flatpickr assets via FilamentAsset::register(), so no php artisan filament:assets is strictly required in a standard Vite theme. If your panel uses a custom theme compiled with @vite, run php artisan filament:assets (and re-run after upgrades) or the range widget renders as a plain text input.
Where to look first
src/Forms/Components/DateRangePicker.php — the field class; this is the source of truth for the fluent API (minDate, maxDate, displayFormat, firstDayOfWeek, rangeSeparator, etc.). Skim the public methods before reading the README.src/Tables/Filters/DateRangeFilter.php — the filter, including the default whereBetween behaviour and the query() extension hook.resources/views/ — the Blade view showing which Alpine component and state keys are actually bound.resources/js/ — the Flatpickr wrapper; check this when formatting or locale behaviour surprises you.First use case — a "created between" table filter
This is the 80% case and the reason to install the package at all:
use CodeWithKyrian\FilamentDateRange\Tables\Filters\DateRangeFilter;
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name'),
TextColumn::make('created_at')->dateTime()->sortable(),
])
->filters([
DateRangeFilter::make('created_at')
->label('Created between'),
]);
}
Two clicks, one date range, whereBetween on the column. That's the whole value proposition — you get a single filter control instead of two DatePickers glued together with custom query logic.
Form field for reports / exports
use CodeWithKyrian\FilamentDateRange\Forms\Components\DateRangePicker;
DateRangePicker::make('period')
->label('Reporting period')
->displayFormat('d M Y')
->minDate(now()->subYears(2))
->maxDate(now()->addYear())
->required()
->columnSpanFull(),
The state arrives in your action() / handle() as an array, not a string — read it defensively:
[$start, $end] = [$data['period']['start'] ?? null, $data['period']['end'] ?? null];
Normalising boundaries before querying
The single most common bug with any range picker: the end date is midnight, so whereBetween silently drops the final day. Normalise in one place — a small helper or a ->dehydrateStateUsing() on the field:
DateRangePicker::make('period')
->dehydrateStateUsing(fn (?array $state) => [
'start' => filled($state['start'] ?? null) ? Carbon::parse($state['start'])->startOfDay() : null,
'end' => filled($state['end'] ?? null) ? Carbon::parse($state['end'])->endOfDay() : null,
]),
Custom filter queries
Override the default whereBetween when the column isn't a plain datetime (e.g. a JSON path, a related table, or a date column you want to compare against endOfDay):
DateRangeFilter::make('created_at')
->query(function (Builder $query, array $state): Builder {
return $query
->when($state['start'] ?? null, fn ($q, $v) => $q->whereDate('created_at', '>=', $v))
->when($state['end'] ?? null, fn ($q, $v) => $q->whereDate('created_at', '<=', $v));
});
Passing the range into a header action
A frequent pairing: range filter on the table + "Export" action that reuses the active filter state rather than duplicating inputs. Pull it from the filter form's live state in the action closure, or hoist the range into a table-level property so both the filter and the action read the same value.
Reusing the widget outside tables
Both classes extend the standard Filament field/filter base classes, so the usual toolbox applies: ->hidden(), ->visible(fn () => auth()->user()->isAdmin()), ->columnSpan(), ->live() + ->afterStateUpdated(). You can drop the field into an Action modal form (Actions\Action::make('export')->form([...])) exactly like any other field.
Testing
Use Livewire's Filament helpers rather than driving Flatpickr through the DOM — the Alpine widget is not what you want to assert against:
livewire(ListOrders::class)
->filterTable('created_at', ['start' => '2026-01-01', 'end' => '2026-01-31'])
->assertCanSeeTableRecords($januaryOrders)
->assertCanNotSeeTableRecords($decemberOrders);
That test is worth writing before you rely on the filter, because it locks in the state key names (start / end) that the package uses.
['start' => ..., 'end' => ...] or ['from' => ..., 'to' => ...]. Check resources/views/ before you write dehydrators or queries — a silent key mismatch yields an always-empty filter that looks like a bug in your query.null, [], or ['start' => null, 'end' => null] depending on path. Guard with filled($state['start'] ?? null) everywhere; never $state['start'] directly.whereBetween on a UTC-stored column will disagree at the edges. For anything billing- or SLA-related, set the panel timezone explicitly and convert on dehydrate, or you'll get "off by one day" reports from users east of UTC.whereDate. whereBetween('date', ['2026-01-01', '2026-01-31']) on a DATE column is fine; on a DATETIME column it is not. Don't use the same filter config for both.filament:assets step, not the package.content globs."^1.0"), read the diff before minor bumps, and keep the fallback in mind: two DatePickers + a ->query() closure reproduces ~90% of this package in an afternoon if it ever goes stale.DateRangePicker and overriding getView() / the Alpine component name is the cleanest way to add presets ("Last 7 days", "This quarter") — do that in one subclass used across your panel rather than sprinkling ->afterStateUpdated() handlers on every form.How can I help you explore Laravel packages today?