asdh/laravel-flatpickr
Laravel Flatpickr integrates the lightweight Flatpickr date and time picker with Blade components and fluent configuration. Bind values to models, set formats, locales, themes, and options, and add responsive pickers to forms with minimal JavaScript boilerplate.
Install
composer require asdh/laravel-flatpickr
Publish what you need, then look at the config first
php artisan vendor:publish --tag=flatpickr-config # config/flatpickr.php
php artisan vendor:publish --tag=flatpickr-assets # public assets (if not using CDN)
config/flatpickr.php is the source of truth for this package — read it before writing any markup. It controls the default flatpickr option set (date format, locale, theme, whether assets are loaded from CDN or locally, and any asset stack the package pushes to). Because the package is a thin wrapper, the config file is where you'll spend most of your integration time.
Wire assets into your layout once
The package typically registers its scripts/styles against a Blade stack. Check the README for the exact stack name and add it to your base layout after your own JS bundle if you intend to override defaults:
{{-- resources/views/layouts/app.blade.php --}}
@stack('scripts')
First use case — a single date field on a form
The lowest-friction path is a plain input plus the package's init hook (component, directive, or data- attribute — check the README, the surface is small):
<input type="text" name="starts_on" id="starts_on" value="{{ old('starts_on') }}">
Then initialise it with the package's global helper so the config defaults apply:
// if the package exposes a JS init hook
window.flatpickr?.('#starts_on', { dateFormat: 'Y-m-d' });
Sanity check before building on it: render one input in a scratch route and confirm the picker opens and the value posts back in the format your migration expects (Y-m-d for a date column). If that round-trips, everything else is configuration.
Pattern 1 — Config-driven defaults, per-field overrides
Set the project-wide display format once in config/flatpickr.php, then treat the picker as a dumb presenter. The single most common production bug here is display format leaking into persistence: flatpickr's dateFormat is what gets submitted, so if users see d/m/Y your controller receives d/m/Y. Use altInput + altFormat so the visible value is human-friendly while the hidden input stays ISO:
// config/flatpickr.php
'options' => [
'dateFormat' => 'Y-m-d', // what is submitted
'altInput' => true,
'altFormat' => 'd M Y', // what the user sees
'allowInput' => true,
],
Pattern 2 — One initialiser for a whole form
Rather than hand-wiring each field, mark inputs declaratively and let a single delegated initialiser sweep the page. This keeps markup self-documenting and survives partials being included in different orders:
<input type="text" name="starts_on" data-flatpickr data-flatpickr-options='{"minDate":"today"}'>
<input type="text" name="ends_on" data-flatpickr data-flatpickr-options='{"minDate":"today"}'>
document.querySelectorAll('[data-flatpickr]').forEach((el) => {
const opts = el.dataset.flatpickrOptions ? JSON.parse(el.dataset.flatpickrOptions) : {};
window.flatpickr(el, opts);
});
Note the data-flatpickr-options payload is JSON inside an attribute — wrap it in single quotes in Blade or you'll fight HTML escaping.
Pattern 3 — Date ranges need onChange, not two independent pickers
For a start/end pair, link them so the second picker can't precede the first. This is the pattern that pays for the package in the first place:
const end = window.flatpickr('#ends_on');
const start = window.flatpickr('#starts_on', {
onChange: ([date]) => { if (date) end.set('minDate', date); },
});
Pattern 4 — Pair every picker with a matching validation rule
The picker is a UI constraint, never a security boundary. Whatever dateFormat (or altInput submission format) you settled on, mirror it in the FormRequest:
'starts_on' => ['required', 'date_format:Y-m-d', 'after_or_equal:today'],
'ends_on' => ['required', 'date_format:Y-m-d', 'after_or_equal:starts_on'],
Pattern 5 — Livewire / Alpine / Turbo integration
Any framework that swaps DOM nodes will orphan a flatpickr instance. Initialise in the lifecycle hook that fires after the swap, not on page load:
// Livewire 3
document.addEventListener('livewire:navigated', initPickers);
// Alpine, per-component
x-init="$nextTick(() => window.flatpickr($refs.date))"
// Turbo
document.addEventListener('turbo:load', initPickers);
Guard against double-init by checking el._flatpickr before calling again, or destroy on teardown.
Pattern 6 — Locale and timezone discipline
Locale is a config concern (locale + the flatpickr l10n bundle); timezone is not handled by flatpickr at all. It emits a naive local string. Decide once whether a date field is a calendar date (store as date, no timezone) or an instant (store as timestamp/datetime and convert explicitly server-side via the app timezone). Don't let Carbon parse() guess.
dateFormat is the submitted value. If validation fails with "does not match format", compare config('flatpickr.options.dateFormat'), the altFormat, and your date_format: rule side by side — three places to keep in sync.altInput changes the DOM. flatpickr hides your real input and injects a sibling. document.getElementById('starts_on') still works for reading the value, but jQuery selectors for styling/labels often break. Style via the generated .flatpickr-input class.config/flatpickr.php run php artisan config:clear. A stale cached config is the most common "my options are ignored" cause.onChange behaviour. Always if (!el._flatpickr) before initialising, and el._flatpickr.destroy() before removing nodes.minDate: 'today' is evaluated at init time. Long-lived tabs and Livewire components that persist for hours will hold a stale "today." Re-init or call set('minDate', ...) on focus if that matters._flatpickr. el._flatpickr.currentYear, .selectedDates, and .config tell you instantly whether your options actually reached the library or died in the Blade attribute/config layer.@push('scripts') block is a realistic escape hatch if a Laravel major version breaks it.How can I help you explore Laravel packages today?