harvirsidhu/filament-timepicker
A Filament form field that adds a customizable time picker to Laravel admin panels, making it easy to select and store time values with a clean, user-friendly input.
Pull it in with Composer; there is no plugin class to register and nothing to add to a panel provider:
composer install harvirsidhu/filament-timepicker
php artisan filament:assets
The filament:assets step is not optional. Since v1.1.0 the suggestion panel ships its own plain-CSS asset (resources/dist/filament-timepicker.css) registered by FilamentTimepickerServiceProvider; skip the command and you get a correctly-behaving but visually naked dropdown. Re-run it after every composer update of this package.
Note the dependency is filament/forms, not filament/filament. If your app is a plain Livewire + Forms setup (no panel), it installs fine; panel users see no difference.
Smallest useful field:
use Harvirsidhu\FilamentTimepicker\SmartTimePicker;
SmartTimePicker::make('start_time')
->interval(15)
->minTime('08:00')
->maxTime('18:00')
->required(),
That is already the first use case worth wiring up: a booking/scheduling form where the operator types 330 or 3p and the canonical 15:30 lands in the column. Where to look first: src/SmartTimePicker.php for the whole public surface (one small class, readable end-to-end), then resources/views/time-picker.blade.php and resources/js/components/smart-time-picker.js when you need to understand why the UX behaves a certain way. The lang file at resources/lang/<locale>/time-picker.php is short and tells you every string you can override.
Relative bounds driven by a sibling field. minTime()/maxTime() accept closures and are genuinely reactive (the v1.1.0 data-config bridge). A common booking shape:
SmartTimePicker::make('start_time')
->minTime(fn (Get $get) => $get('opening_time') ?? '08:00')
->maxTime(fn (Get $get) => $get('closing_time') ?? '18:00'),
SmartTimePicker::make('end_time')
->minTime(fn (Get $get) => $get('start_time'))
->maxTime(fn (Get $get) => $get('closing_time') ?? '18:00'),
Duration derivation. durationFrom() points at a sibling holding a duration in minutes and both labels the suggestions (09:00 – 09:30) and floors the picker at the start time; defaultDuration() auto-fills the field on change, now clamped into [minTime, maxTime] rather than only capped at the ceiling:
SmartTimePicker::make('ends_at')
->durationFrom('duration')
->defaultDuration(fn (Get $get) => $get('duration') ?? 30),
Reactivity that actually fires. ->live() and afterStateUpdated() work on commit — the field binds via applyStateBindingModifiers() so a ->live() field emits a real $entangle(path, true). Use it for summary panels, price calculation, conflict checks:
SmartTimePicker::make('start_time')
->live()
->afterStateUpdated(fn (Set $set, $state) => $set('end_time', ...)),
Be aware the hook fires on commit, not on every keystroke — that is the intended behaviour, since intermediate strings like 3 or 33 are not times.
Strictness is two layers. minTime()/maxTime() are enforced unconditionally; strict(true) adds only the interval-grid check on top. Bounds are evaluated first, so an out-of-window value gets the useful min_time/max_time message rather than a confusing "off grid" one. Reach for strict() when slot alignment matters (appointments), not when you just want opening hours respected.
Migration from Filament's stock TimePicker. Swap the class and keep your calls: native() and timezone() exist as deliberate no-ops for drop-in parity. Do not expect ->timezone('America/New_York') to shift anything — the storage contract is wall-clock H:i, and a 3 PM slot stores as 15:00 regardless.
Custom themes. If you previously had an @source line pointing at this package's views, you can delete it — it is a no-op now that the panel is a published asset reading Filament's runtime --gray-* variables. Your panel's colours still apply.
Testing your own form. Assert the stored value as H:i, not as a Carbon instance in some zone. A regression test along the lines of "stores appointment slot times as wall-clock" is cheap insurance and matches the package's own guarantee.
minTime()/maxTime() are now enforced. Values that previously slipped through via paste, $set, or an importer are rejected both client-side (the box snaps back to the last good value) and server-side (a validation rule with its own message). Audit existing data and import paths before rolling out.Enter in a closed picker still commits typed text but then submits the form. If you have a multi-field form where users habitually hit Enter, make sure your submit handling is intentional.interval()/minTime() looks stale, inspect the data-config sibling <div> in DevTools. The component root carries wire:ignore (load-bearing — it stops Livewire re-initialising the teleported <ul> outside Alpine scope after a roundtrip), which also freezes x-data. Config therefore arrives through that sibling attribute, which Livewire can morph. If the attribute is right but the dropdown is wrong, the bug is in RECONFIGURABLE/watchConfig() in the JS; if the attribute itself is stale, the problem is in your closure.wire:ignore, and avoid Blade conditionals that tear down and recreate the field's DOM. Filament's hidden()/disabled() are fine — disabled and aria-invalid are kept current across roundtrips by explicit code, precisely because the root is ignored.left/top/width written onto the panel element — not at a :style binding. Moving it to a string :style binding would wipe the display/opacity that x-show/x-transition set on the same node. It re-measures every frame while open (Floating-UI-style autoUpdate), so a field that moves during a modal animation or a late font load should self-correct; if it doesn't, that loop is the place to look.Support\TimeParser is authoritative; parse()/toDisplay() in the Alpine component mirror it for instant feedback. Change parse rules in both and add a row to tests/Fixtures/parse-cases.json, which both the PHP and JS suites iterate — that fixture is what makes drift fail CI. dehydrateStateUsing stays a pure parse normalizer; don't push validation into it.SmartTimePicker::getAlpineConfig() and RECONFIGURABLE in the JS, or the new option will stay pinned to its first-render value.harvirsidhu-filament-timepicker::, not the Spatie shortName — use that prefix everywhere (PHP __(), the Blade view, and the durationLabels passed into x-data). Publish the lang file to localise duration words; there is no hardcoded English in the JS.displayFormat() uses PHP date() tokens with backslash-escape support, and it is honoured on both sides — including in the min_time/max_time validation messages. Pick something unambiguous ('H:i' or 'g:i a') if your users skew international, and keep seconds() off unless your column is actually H:i:s.composer.json and keep your own wall-clock regression test. The package is domain-agnostic by design — it knows nothing about availability, clinics, or appointments — so all business rules belong in your form, not in the field.How can I help you explore Laravel packages today?