
Summary
Weekly availability / opening-hours editor with per-day toggles, from/to time slots, optional break entries, copy-to-weekdays, searchable timezone selector, and locked days. Time pickers use the compact FlexTimeSegmentsField dropdown (hour + minute columns). Default control size issm; the timezone selector always renders at md.
| Class | Bjanczak\FilamentFlexFields\Filament\Forms\Components\ScheduleField |
| State type | array<timezone?: string, days: array<string, array{enabled: bool, slots: list<array{from: string, to: string, type?: string>>}>} |
| Model cast | 'opening_hours' => 'array' or 'json' |
| FieldType | (no dedicated FieldType mapping yet — use the class directly) |
| Playground | schedule-field slug in Flex Fields playground |
Basic usage
State format
Day keys use three-letter English abbreviations:mon, tue, wed, thu, fri, sat, sun.
Times are HH:MM strings in 24-hour format (09:00, 17:30). On save, invalid slot entries are dropped and times are zero-padded.
Minimal example
Split shift with lunch break
Slot type | UI | Notes |
|---|---|---|
slot (default) | Work slot row with briefcase icon | Counted toward min/max slot limits |
break | Dashed break row with clock icon | Also validated for time order and overlap |
timezone(null) hides the selector, omit the timezone key from state — it is not required or validated.
Default state
The field default callsScheduleField::defaultSchedule():
Configuration API
All methods acceptClosure for dynamic configuration.
days(array|Closure $days)
Which days to render. Invalid day codes are ignored. Empty array falls back to all seven days.
Default: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'].
timezone(string|Closure|null $timezone)
Default timezone identifier when the selector is shown, or null to hide the timezone block entirely.
Default: 'UTC' (selector visible).
md size regardless of the schedule field size().
timeStep(int|Closure $minutes)
Minute step for FlexTimeSegmentsField dropdowns (From / To). Clamped to 1–60.
Default: 5.
minSlots(int|Closure $count) / maxSlots(int|Closure $count)
Per-day slot limits (includes both work slots and breaks).
Defaults: 1 / 10.
requireSlotsForEnabledDays(bool|Closure $condition = true)
When true (default), each enabled day must have at least minSlots() valid entries.
When false, an enabled day may have zero slots (useful for “open but hours TBD” flows — use carefully).
allowCopyToWeekdays(bool|Closure $condition = true)
Shows Copy to weekdays on the copy source day.
Default: true.
copySourceDay(string|Closure $day = 'mon')
Which day row displays the copy button. Must be one of mon … sun.
Default: mon.
workdays(array|Closure $days)
Target days for Copy to weekdays. Invalid entries are filtered. Empty after filter falls back to Mon–Fri.
Default: ['mon', 'tue', 'wed', 'thu', 'fri'].
enabled flags.
lockedDays(array|Closure $days)
Days that cannot be toggled on/off. Shows a lock icon instead of the switch. Locked days still display their schedule when enabled in state.
Only days present in days() are kept.
variant(string|Closure $variant)
Visual container style.
| Value | Description |
|---|---|
primary | Default |
secondary | Secondary surface |
soft | Soft background tokens |
flat | No shadow / minimal chrome |
size(string|ControlSize|Closure $size)
Size of time inputs and day rows. Default: sm.
Timezone selector remains md.
readOnly(bool|Closure $condition = true) / disabled(bool|Closure $condition = true)
Inherited from Filament. Disables toggles, time pickers, copy, and slot toolbar actions.
focusOutline(bool|Closure $condition = true)
Inherited from HasFieldFocusOutline. Default: false. Adds focus ring on time input shells when enabled.
Public helper methods
| Method | Returns | Description |
|---|---|---|
getRequiredValidationRule() | string|Closure | Returns 'nullable' — emptiness checked inside schedule validator when required() is set |
getDays() | list<string> | Normalized day codes to render |
showsTimezoneSelector() | bool | Timezone block visible |
getDefaultTimezoneIdentifier() | string|null | Default / fallback timezone |
getTimeStep() | int | Minute step (1–60) |
getMinSlots() / getMaxSlots() | int | Slot limits |
shouldAllowCopyToWeekdays() | bool | Copy button enabled |
getCopySourceDay() | string | Copy source day code |
getWorkdays() | list<string> | Copy target weekdays |
shouldRequireSlotsForEnabledDays() | bool | Min slots enforced |
getLockedDays() | list<string> | Locked day codes |
isDayLocked(string $day) | bool | Single day lock check |
getVariant() / getSize() | string | Resolved styling |
defaultSchedule(?string $timezone, ?array $days) | array | Static default state builder |
normalizeState(mixed $state) | array | Canonical schedule array |
isEmptyState(mixed $state) | bool | True when no day keys |
stateMatches(array $normalized, mixed $state) | bool | Compare normalized JSON |
getResolvedTimezoneIdentifiers() | list<string> | Allowed IANA identifiers |
getTimezoneOptionsForJs() | list<array<id,label,offset>> | Timezone dropdown data |
getAlpineConfiguration() | array | Alpine + validation config |
getWrapperClasses() | array<string, bool> | Root CSS class map |
Validation
Built-in rule runs on submit (and when Livewire validates). Custom Filamentrequired is mapped to nullable at rule level — emptiness is checked inside the schedule validator when required() is set.
| Check | When |
|---|---|
| Required field | All configured days missing from state |
| Timezone required | Selector shown but value empty / invalid |
| Timezone in list | Must be in PHP timezone_identifiers_list() |
| Missing day | Configured day absent from state |
| Invalid slot shape | Non-array slot entry |
| Invalid time | Not parseable as HH:MM |
from before to | End time must be after start |
| Min slots | Enabled day has fewer than minSlots() |
| Max slots | Enabled day exceeds maxSlots() |
| Overlap | Two slots on the same day overlap in time |
from_before_to, min_slots, max_slots, overlap).
Disabled days skip slot validation entirely.
Model & persistence
Recipe: restaurant — weekdays + lunch break
Recipe: support desk — weekdays only, no weekends in UI
Recipe: always-closed weekends (locked)
Recipe: no timezone selector (local hours)
Recipe: 30-minute steps, larger time inputs
Recipe: read-only preview on view page
UI behaviour
| Feature | Detail |
|---|---|
| Day toggle | Switch per day — expands/collapses slot editor with animation (is-day-animated) |
| Time pickers | FlexTimeSegments dropdown — separate hour and minute columns |
| Add slot / Add break | Compact toolbar buttons per day |
| Remove slot | Trash icon when more than minSlots() or day would still satisfy rules |
| Copy to weekdays | Visible only on copySourceDay() when allowCopyToWeekdays(true) and workdays overlap |
| SSR + hydration | Server-rendered markup for enabled days; Alpine replaces with live UI (displayReady) |
| State sync | wire:ignore + $entangle — full schedule JSON synced to Livewire |
CSS classes
| Class | Role |
|---|---|
fff-schedule-field | Root wrapper |
fff-schedule-field--{sm|md|lg} | Size modifier (time inputs) |
fff-schedule-field--{variant} | Variant modifier |
fff-schedule-field--has-copy-column | Grid includes copy column |
fff-schedule-field__days | Bordered week container |
fff-schedule-field__day | Single day block |
fff-schedule-field__day-header | Day label + status + copy + switch |
fff-schedule-field__slot | Work slot row |
fff-schedule-field__slot--break | Break slot row (dashed) |
fff-schedule-field__time-shell | FlexTimeSegments shell |
fff-schedule-field__action-btn | Add slot / Add break buttons |
fff-schedule-field__day-switch | Inline switch wrapper |
fff-timezone-field, teleported menu classes).
Assets
Lazy-loaded bundles (FlexFieldAssets::stylesheetsFor('schedule-field')):
flex-text-inputswitchteleported-menutimezone-fieldflex-time-segments
schedule-field(main coordinator)flex-time-segments(preloaded per slot — shared chunk)
wire:ignore on the field root. After deploy, run:
Playground
Slug:schedule-field
| Demo field | Shows |
|---|---|
| Default block | Weekday 09:00–17:00 default, timezone selector |
| Copy-to-weekdays | copySourceDay('mon') + workday targets |
| Locked weekends | lockedDays(['sat', 'sun']) |
| No timezone | timezone(null) — local hours only |
| Size / variant | size('md'), variant('soft') |
FLEX_FIELDS_PLAYGROUND=true) and open Flex Fields Playground → Schedule field.
Implementation notes
- Normalization:
ScheduleNormalizer— pads times, drops invalid slots, coercestypetoslotorbreak. - Validation:
ScheduleValidator— overlap detection sorts slots by start time. - Day constants:
ScheduleDays::ALL,ScheduleDays::WEEKDAYS. - Click-outside on the field closes open time menus and the timezone dropdown.
- Mobile layout reflows day header (copy button moves below label row).