asmit/fila-calendar
A Filament calendar package for Laravel that adds calendar views and event management to your admin panel. It integrates with Filament resources so you can schedule, display, and manage events with minimal setup, ideal for dashboards and scheduling workflows.
asmit/fila-calendar adds an always-visible inline calendar to Filament forms and infolists. Unlike Filament's stock DatePicker (a popover trigger), the calendar grid is rendered directly in the form, which makes it a good fit for booking screens, availability editors, and scheduling admin panels where seeing the month at a glance matters.
Install and register
composer require asmit/fila-calendar
The package ships a service provider that registers its Blade views. There is usually nothing to publish for a first run. If you need to restyle the grid:
php artisan vendor:publish --tag=fila-calendar-views
php artisan vendor:publish --tag=fila-calendar-config # if a config file is offered
If the package ships compiled JS/CSS, run Filament's asset command after install and after every upgrade (php artisan filament:assets, or php artisan filament:upgrade on older Filament versions).
First use case: a single-date form field
use Asmit\FilaCalendar\Forms\Components\CalendarField;
CalendarField::make('starts_on')
->label('Start date')
->mode('single')
->minDate(now())
->required();
First use case: a read-only calendar in an infolist
use Asmit\FilaCalendar\Infolists\Components\CalendarEntry;
CalendarEntry::make('availability')
->mode('range');
The exact class names and namespaces follow the package's convention but can shift between releases. Check
src/or the README of the installed version before wiring it into a resource — the mode API is the part most likely to change.
Where to look first
single, range, and multi-range return different state shapes, and that difference drives everything else.composer.json — for the supported Filament major version. With a September 2026 release, expect Filament v4 support; verify before dropping it into a v3 panel.Mode drives your column type
| Mode | Persisted shape | Column |
|---|---|---|
single |
string (Y-m-d) |
date |
range |
['start' => ..., 'end' => ...] |
json |
multi-range |
[['start' => ..., 'end' => ...], ...] |
json |
Cast JSON columns to array in the model so the field hydrates cleanly on edit:
protected $casts = [
'availability' => 'array',
];
Booking / scheduling form
Pair the calendar with reactive state so the rest of the form responds to what the user clicks:
CalendarField::make('slot_range')
->mode('range')
->live()
->afterStateUpdated(function (?array $state, Set $set) {
$set('duration_days', $state
? Carbon::parse($state['start'])->diffInDays(Carbon::parse($state['end'])) + 1
: null);
})
->required();
Availability / blackout editor
Multi-range is the strongest use case: an admin paints several windows on one month and saves them as a JSON array. Normalize before persisting so you never store half-open ranges:
->mutateDehydratedStateUsing(fn (?array $state) => collect($state)
->filter(fn ($r) => filled($r['start'] ?? null) && filled($r['end'] ?? null))
->sortBy('start')
->values()
->all())
Pairing with table columns
There is no first-class table column in the package. Render a summary with a TextColumn:
TextColumn::make('availability')
->formatStateUsing(fn ($state) => collect($state)
->map(fn ($r) => "{$r['start']} → {$r['end']}")
->join(', '))
->wrap();
Using it as a filter
Reuse the form field inside a ->form() schema on a Filter, but constrain it to a form-friendly mode (single or range) and add explicit query logic in ->query() — the field supplies state, not filtering.
Theming to match the panel
Because the grid is inline, it inherits less of Filament's chrome than a popover. Publish the view and swap hard-coded colors for Filament's CSS custom properties (--primary-500, --gray-100, etc.) so light/dark mode and panel color changes propagate.
Livewire testing
State is a plain array/string on the component, so standard Filament test helpers work:
livewire(CreateBooking::class)
->fillForm(['slot_range' => ['start' => '2026-10-01', 'end' => '2026-10-05']])
->call('create')
->assertHasNoFormErrors();
State shape is not uniform across modes. This is the single biggest source of bugs. A single field yields a string; range yields an associative array; multi-range yields a list of arrays. Code that "just works" in single mode (e.g. Carbon::parse($state)) will fatal in range mode. Branch on mode or normalize in a custom cast.
required() on an array field is weaker than you expect. An empty multi-range often arrives as [], which passes required. Add ->rule('array|min:1'), and validate each range's endpoints separately.
Timezone drift. The calendar grid is rendered client-side, so date arithmetic can pick up the browser timezone while your validation rules use config('app.timezone'). Pick one storage convention (store Y-m-d strings, not datetimes) and stick to it; if the package exposes a timezone setter, verify it actually reaches the JS layer.
Empty vs. null on edit. Hydrating a JSON column that is null sometimes yields [] and sometimes null depending on the cast. Guard both in any formatStateUsing or afterStateUpdated callback.
Asset and view caching after upgrades. With a small, fast-moving package, a stale compiled view is a common "my CSS change did nothing" cause. After upgrading: php artisan view:clear && php artisan filament:assets.
No built-in table column or filter. Budget time for the TextColumn formatter and filter query yourself; don't assume parity with DatePicker's table/filter surface.
Maturity risk. Six stars, zero dependents, and a single-maintainer release cadence. Pin the version in composer.json ("asmit/fila-calendar": "^1.0" rather than *), read the release notes before bumping, and wrap the field in your own thin factory class if you use it in more than a handful of places — that gives you one place to patch if the mode API changes or the package goes quiet.
Extension points. Prefer ->extraAttributes() and a published Blade view over forking. The Alpine component is the fragile part; avoid depending on internal method names in your own JS. If you need click-through behavior the package doesn't expose, handle it via ->afterStateUpdated() on the Livewire side rather than reaching into the grid's JavaScript.
Debugging tip. When clicks don't update the form, drop a ->live() plus dump($state) into afterStateUpdated(). If $state never changes, the issue is the JS-to-Livewire bridge (asset version, caching, or a Filament version mismatch), not your schema. If $state updates but the saved record is wrong, the problem is dehydration — check mutateDehydratedStateUsing and your model cast.
How can I help you explore Laravel packages today?