savannabits/filament-flatpickr
Filament Flatpickr swaps Filament's default date picker for the Flatpickr library, adding date ranges, time selection, locales, and flexible formatting to form fields and filters. Simple to install and configure per field.
Install and verify before you commit
composer require savannabits/filament-flatpickr
Two diligence items before this lands in main:
savannabits/filament-flatpickr but the linked repo is coolsam/flatpickr. Confirm these are the same artifact, who holds publish rights, and whether this is a fork chain. Do this in writing; don't skip it because the install "worked."composer require savannabits/filament-flatpickr:^X.Y with an exact constraint (no * or floating ranges). This package is a single-maintainer wrapper around Flatpickr and will lag Filament majors.Filament v3+ auto-discovers the service provider. If the picker renders as a plain text input with no calendar, assets didn't register — run:
php artisan filament:assets
php artisan vendor:publish --tag="filament-flatpickr-config" # only if you need to override defaults
Then hard-refresh the browser. Do this in a real Vite production build in staging, not just php artisan serve. The single most common "it works on my machine" failure is asset registration succeeding under the dev server and failing under npm run build.
Where to look first
vendor/savannabits/filament-flatpickr/src/Forms/Components/ — the field class and its fluent methods. This is a small file; read it in full before writing code against it. Method names differ between versions (->range() vs ->mode('range'), ->format() vs ->dateFormat()), so trust the source over the README.config/filament-flatpickr.php (after publishing) — global defaults like locale and format.public/ — confirms the asset pipeline actually shipped the Flatpickr bundle.First use case: pick the one the native picker is bad at
Don't replace every DateTimePicker in the app. Pick a single field where Filament's stock picker is genuinely awkward — a date-range filter on a reporting dashboard, or a booking window with an event start/end on an internal admin form — and prove the whole matrix there (create, edit-existing hydration, validation re-render, non-UTC timezone). That's your spike exit criterion.
Basic field usage
use Savannabits\FilamentFlatpickr\Forms\Components\Flatpickr;
// verify exact namespace against src/ — it may differ by version
Flatpickr::make('published_at')
->label('Publish at')
->dateFormat('Y-m-d H:i') // maps to Flatpickr's dateFormat option
->time_24hr()
->required(),
Range selection with an array state
Range mode dehydrates to a two-element array, which changes the validation shape you write:
Flatpickr::make('stay_window')
->label('Check-in → Check-out')
->mode('range') // or ->range() if the version aliases it
->dateFormat('Y-m-d')
->rules(['required', 'array', 'size:2'])
->dehydrateStateUsing(fn ($state) => $state), // keep as array; map in the model
Store the array in a JSON column or split it in mutateFormDataBeforeSave. Don't assume a scalar state.
Time-only and multiple-date
Flatpickr::make('shift_start')->mode('time')->dateFormat('H:i')
Flatpickr::make('campaign_dates')->mode('multiple')->dateFormat('Y-m-d')
Friendly display vs. storage format
altInput is the reason many teams reach for this package: the operator sees 12 March 2026, you persist 2026-03-12. Configure both formats explicitly so there's no ambiguity about which string hits the database:
Flatpickr::make('starts_at')
->dateFormat('Y-m-d') // storage
->altFormat('d F Y') // display
->altInput() // enable friendly display
Reactive fields and dependent state
The field is a normal Filament field, so ->live() and ->afterStateUpdated() work as you'd expect. Use them for cascades like "shift end must be after shift start":
Flatpickr::make('starts_at')->live()->afterStateUpdated(fn ($state, Set $set) => $set('ends_at', $state)),
Flatpickr::make('ends_at')->minDate(fn (Get $get) => $get('starts_at')),
Validation and constraints via the fluent API
->required(), ->minDate(), ->maxDate(), and ->disabledDates() all map to Flatpickr options — but they are client-side hints only. Always pair disabledDates / minDate / maxDate with a matching server-side ->rules(['after_or_equal:...']). The Flatpickr UI is not a security boundary; an operator can type a date directly into the text input.
Repeaters and Builders
Wrap each row's field so Livewire can key it uniquely and re-mount the widget correctly. The picker is imperative JS under an Alpine component; without stable keys you get pickers attaching to the wrong row after add/remove/reorder.
Filters and dashboard surfaces
Use the field inside table filter schemas for consistency, but be aware table filters re-render on every keystroke with wire:model.live. Test the range filter against rapid state changes — that's where you'll see the imperative/declarative mismatch surface.
Wrap it, always
namespace App\Filament\Fields;
class DateRange extends \Savannabits\FilamentFlatpickr\Forms\Components\Flatpickr
{
// defaults, locale, format
}
Every schema entry uses App\Filament\Fields\DateRange::make(...). Reverting to native Filament\Forms\Components\DatePicker becomes a one-line change in one class instead of a refactor.
Assets fail silently, in production only. The picker "degrades" to a bare text input when Flatpickr's JS fails to load — no error, just ugly UX that operators won't report. Verify in the built bundle: npm run build, php artisan filament:assets, load in staging with DevTools Network pinned to the Flatpickr chunk.
Livewire state loss after validation. The classic failure: submit an invalid form, Filament re-renders, and the picker shows empty even though the state is still there — or worse, the picker shows a date the model doesn't have. Reproduce deliberately (create → submit invalid → correct → submit) and check what's in the wire:model payload before blaming the package. Alpine initialization order is the usual culprit; a re-init via x-init on the wrapper often fixes it.
Timezone drift writes dates one day off. Server TZ vs. browser TZ vs. the model's cast. The UI looks right and the DB is wrong — standard QA won't catch it. Test the field set to America/Los_Angeles and Asia/Tokyo against a UTC server. Store UTC, convert on display, and document the rule once for the panel rather than per-field.
Range state is an array, not a string. Validation size:2, cast to JSON, and any mutateFormDataBeforeSave must account for it. If you reuse your scalar after_or_equal rule on a range value you'll get confusing "Invalid date" errors at save time.
disabled, readonly, dehydrated(false) all behave differently. Test each explicitly — the JS init for Flatpickr on a disabled field is a known edge case in thin wrappers. Also test inside a Repeater and on a filter.
Bus factor is one. No SLA, no third-party production validation, 102 stars, zero dependents. Before this becomes load-bearing on a revenue-relevant screen, plan to vendor or fork. The codebase is thin enough that a fork is cheaper than waiting on an upstream fix during an incident.
Filament upgrades break it. Budget half a day per Filament major to re-verify this field on its own, independent of the rest of the upgrade. Add it to the upgrade checklist as a line item, and re-verify in staging with the production asset build.
Locale and format consistency. Set locale and dateFormat once in the published config (or your wrapper class) rather than per-field. Inconsistent formats across the panel is the fastest way to produce off-by-one-day bugs, because different fields round differently at midnight boundaries.
Debugging checklist when something's off
->state() in an afterStateUpdated — what actually landed in Livewire?$refs, x-init order).wire:key present and stable inside any Repeater/Builder row.Extension points are limited. You can reach the underlying Flatpickr options via the fluent API and extraInputAttributes, and you can override the Blade view with ->view(). But there's no plugin hook for custom calendar behavior. If you need recurrence rules, multi-resource scheduling, or drag-and-drop rescheduling, this is the wrong tool — reach for FullCalendar or a custom build. This package is a form-field polish lever, not a scheduling platform.
How can I help you explore Laravel packages today?