Datetime Picker
The <x-wirekit:: component picks a moment: a date and a time of day. Its trigger looks like a field and shows the moment it holds, "Oct 27, 2026 at 10:30 AM". It opens a dialog with a calendar and a list of times beside it. The value is written YYYY-MM-DDTHH:MM, in the shape a datetime-local field submits, and a form receives it under the field's name.
Usage
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
A value takes both halves. A day picked first shows on the trigger, and the focus moves to the list of times, but the field stays empty until a time is picked too: half a moment cannot be written as one, and a quiet midnight would be wrong. Once there is a value, another day changes it at once and the dialog stays open; a time completes it and closes the dialog.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
A date object works as the value too, such as a model attribute with a datetime cast. It is read as the wall-clock time it names in your application's time zone.
Confirmation
With with-confirmation the dialog keeps the day and the time it is given until Select is pressed. Cancel, Escape or a press outside leave the value as it was, and the trigger shows the old value until then.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Default time
default-time gives a day picked without a time that time at once, so a single pick in the calendar makes a value.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Times
interval sets the minutes between two times in the list, 30 by default. min-time and max-time set the first and the last time of every day, and a time outside them is not in the list.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
time-format shows the times on a 12-hour or a 24-hour clock. auto, the default, follows your application's locale.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
The time list is a list of times on the wall clock. A day on which the clocks change offers the same times as any other day, and the value is the time it shows.
Limits
min and max take a moment, a day, a date object, today or now. The calendar closes the days before min and after max, and on those two days the list closes the times outside them. max written as a day leaves the whole of that day open.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
unavailable closes days, times and spans: a day (2026-10-24), a time or a span on every day (12:00, 12:00-13:00), and a moment or a span on one day (2026-10-23T10:00, 2026-10-23T15:00-16:00), both ends of a span included. A date object closes the moment it names, as an existing booking does. Write a list, or the entries separated by commas.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
A closed day or time stays in reach of the keyboard and is named as unavailable, and nothing picks it. The limits steer what a reader can pick and nothing more: validate the value on the server as well.
Typing the time
time="input" puts a field under the calendar where the time is typed, instead of the list beside it. The field holds the hour, the minute and, on a 12-hour clock, AM or PM, each a segment of its own, in the order and with the separators of your application's locale. The button at the end of the field opens the list of times as suggestions, with the same limits, and a time picked there fills the segments.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
A digit sets a segment and moves on once no other digit fits: on a 12-hour clock 1 waits for 10, 11 or 12, and 3 moves to the minutes at once. The arrow keys step a segment round its range, from 59 back to 00 without changing the hour, Home and End go to its first and its last value, a and p set AM or PM, and Backspace empties it. On a touch screen the keyboard opens with digits.
A typed time outside min-time and max-time, or closed by unavailable, min or max on the day picked, is not taken, and a sentence under the field says why: type 7:00 AM above, and it names the times from 8:00 AM to 6:00 PM. A typed time waits for Select, as with with-confirmation, so the field never receives a time that is half typed.
Typing the whole moment
type="input" makes the trigger a field where the whole moment is typed: the day, the month, the year, the hour, the minute and, on a 12-hour clock, AM or PM, each a segment of its own, in the order and with the separators of the locale. A German page asks 27.10.2026, 14:45, an American one 10/27/2026, 2:45 PM. The button at the end of the field opens the dialog, and the field and the dialog show each other's moment.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
A segment takes digits as a typed time does, and the year takes four. A moment typed whole and open is taken at once, as a native field takes it, so wire:model.live hears each one. A day its month does not have, a day closed by min, max or unavailable, and a closed time are not taken: the field holds no moment, and a sentence under it says why. A moment pasted into any segment fills the field, written as a timestamp (2026-10-27T14:45, an offset dropped) or as the locale writes it.
With range the start and the end are typed in two fields, named Start and End, with one button beside them that opens the dialog for both. The two stand side by side, and on a narrow screen one above the other. A range typed whole is taken at once when the end comes after the start and the range covers as many days as min-range and max-range allow, both ends counted; until then the field of the end says why. The dialog opens on what the two fields hold, a start typed without its end among it.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
With a custom trigger the trigger stays the caller's button.
Range
range picks two moments, a start and an end. The calendar shows two months and picks the two days, and below it a row for each end takes its time: a field that opens the list of times, or with time="input" the time typed in segments. A range always waits for Select range, which stays closed until both ends have a day and a time and the end comes after the start. On one day the times decide, and the sentence under the times says why.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
The trigger writes the range from its start to its end, "Oct 27, 2026 at 9:00 AM – Nov 1, 2026 at 5:00 PM", and the end's time alone when both ends fall on one day. default-time gives days picked by hand their times, one for both ends or 09:00/17:00 for each, and min-range and max-range limit the days a range covers, as on the calendar.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
With presets the calendar lists ranges such as "Last 7 days" beside it, as on the calendar. A preset takes its whole days, from the first time of a day to the last, unless default-time names the two.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Binding a range
The form receives name[start] and name[end], each YYYY-MM-DDTHH:MM, and name[preset] with presets. wire:model binds the two ends as {property}.start and {property}.end, so the property is an array with the two keys or a Pushery\, which keeps the time of each end and the preset:
use Pushery\WireKit\Values\DateTimeRange;
public DateTimeRange $trip;
public function mount(): void
{
$this->trip = DateTimeRange::from('2026-10-27T09:00/2026-11-01T17:00');
}
<x-wirekit::datetime-picker name="trip" label="Trip" range :value="$trip" wire:model.live="trip" />
Validation reads the ends as trip.start and trip.end. :value takes start/end as text, an array with start and end, the two as a list, or a range object.
Opening month and time
Without a value the calendar opens on the current month and the list of times on the current time. open-to names another moment, or a day, for both.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Clearing
clearable adds a button to the trigger that empties the field, while it holds a value.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Hint and error
A hint stands under the field and describes the trigger. An error takes its place, marks the trigger invalid and is announced. After a failed validation the error is read from the error bag by name, so a form needs no :error of its own.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Local time at the airport.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
The arrival must come after the departure.
Calendar options
The calendar in the dialog takes the options of the calendar component: months, with-today, selectable-header, week-numbers, week-starts-on and fixed-weeks. See the calendar for each.
| Week number | Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|---|
Sizes
size sets the height of the trigger: sm, md, the default, or lg, as on the other fields.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Custom trigger
The trigger slot replaces the field with a trigger of your own, a button that says what it does. The picker wires it to the dialog, and its content names it.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
On a phone
Below the kit's sm width, 640 pixels, and on a screen less than 480 pixels tall, as a phone held sideways, the dialog opens as a modal over the page rather than below the trigger: centered over a backdrop, inside the safe area of a phone with a notch, with the page behind it held still. The days grow to the size of a touch target as far as the width allows, a range shows one month with its presets as a select above it, and the list of times gives way to a select, which the phone opens as its own wheel; a typed time keeps its segments. A pick always waits for Select there, with or without with-confirmation, and a tap on the backdrop closes the dialog as Escape does. A dialog taller than the screen scrolls in itself with Cancel and Select in sight. The screen is read when the dialog opens, and a tablet keeps the panel below the trigger whichever way it is held.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun |
|---|---|---|---|---|---|---|
Set the preview to 390 pixels and open the picker to see the modal.
Inside a form
The picker renders a hidden field with the given name, which a form submits as YYYY-MM-DDTHH:MM. required stops an empty submit as a native field does, with the browser's own message under the field. A form reset returns the picker to the value it started with. disabled takes the field out of the form, and readonly shows the value and opens nothing.
<form method="POST" action="/meetings">
@csrf
<x-wirekit::datetime-picker name="starts_at" label="Starts at" required />
<x-wirekit::button type="submit">Save</x-wirekit::button>
</form>
With Livewire
wire:model binds the moment, written YYYY-MM-DDTHH:MM. Pass the bound property as :value as well: the picker shows the moment it is given, and a moment the server sets reaches it.
<x-wirekit::datetime-picker name="starts_at" label="Starts at" :value="$startsAt" wire:model.live="startsAt" />
The answer to a pick of the reader's own changes nothing on the screen, so an open dialog stays open while the server answers.
Localization
The picker writes its moment in your application's locale, the one config('app. carries: the day, the time and the word between them on the trigger, and the months and weekdays of the calendar. The server writes the trigger the way the script does, so its text does not change when the page starts, and the language of the reader's browser plays no part.
locale writes the moment in another locale, a BCP 47 tag such as fr or en-US: for a schedule that belongs to another region than the page, or for a reader whose language your application keeps in the session.
| lun. | mar. | mer. | jeu. | ven. | sam. | dim. |
|---|---|---|---|---|---|---|
<x-wirekit::datetime-picker name="departure" label="Departure" :locale="session('locale')" />
The other texts of the picker, such as Cancel, Select and the placeholder, stay in your application's language and come from WireKit's translations. time-format sets a 12-hour or a 24-hour clock where the locale's own does not suit, and week-starts-on="locale" starts the calendar's week on the locale's first day.
Timezones
The picker works with wall-clock time, as a datetime-local field does: the moment the reader picks is the moment the field submits, 2026-10-27T14:00, and nothing is converted in the browser. A date object, or a moment written with its offset as to writes one, is read in your application's time zone, config('app., and Livewire reads the moment that comes back in that zone too.
When your readers live in other time zones, keep the bound property as text in the reader's zone and convert it when you load and store it:
use App\Models\Meeting;
use Carbon\CarbonImmutable;
use Livewire\Component;
class EditMeeting extends Component
{
public Meeting $meeting;
// The reader's time zone, from their profile.
public string $zone = 'America/New_York';
public string $startsAt = '';
public function mount(): void
{
$this->startsAt = $this->meeting->starts_at->setTimezone($this->zone)->format('Y-m-d\TH:i');
}
public function save(): void
{
$this->meeting->update([
'starts_at' => CarbonImmutable::createFromFormat('Y-m-d\TH:i', $this->startsAt, $this->zone)->utc(),
]);
}
}
<x-wirekit::datetime-picker name="starts_at" label="Starts" :value="$startsAt" wire:model="startsAt" />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string|null |
null |
Name of the hidden form field. Ids derive from it |
id |
string|null |
null |
Id of the trigger |
label |
string|null |
null |
The field's label |
value |
string|DateTimeInterface|array|null |
null |
The moment, YYYY-MM-DDTHH:MM, or a date object, read in the application's time zone. With range: start/end, an array with start and end, or a DateTimeRange. See Binding a range |
range |
bool |
false |
Two moments, a start and an end, taken together with Select range. See Range |
type |
string |
'button' |
button, a trigger that opens the dialog, or input, a field where the whole moment is typed, two with range, with a button that opens the dialog. Not with a custom trigger. See Typing the whole moment |
placeholder |
string|null |
null |
What the trigger shows without a value; "Select a date and time" by default, "Select a date and time range" with range |
hint |
string|null |
null |
A hint below the field |
help |
string|null |
null |
An explanation in a tooltip beside the label |
error |
string|null |
null |
An error message; read from the error bag by name as well |
announceError |
bool|null |
null |
Whether an error is announced when it appears |
size |
string |
'md' |
The height of the trigger: sm, md or lg |
disabled |
bool |
false |
Disables the picker and leaves it out of the form |
readonly |
bool |
false |
Shows the value and opens nothing |
required |
bool |
false |
Stops an empty submit with the browser's message |
clearable |
bool |
false |
A button that empties the field |
withConfirmation |
bool |
false |
The day and the time wait for Select, as a range always does. See Confirmation |
defaultTime |
string|null |
null |
The time a day picked without one takes, HH:MM; with range one for both ends or HH:MM/HH:MM |
time |
string |
'list' |
list, a list of times beside the calendar, or input, a time typed in segments with the list as suggestions. See Typing the time |
interval |
int |
30 |
Minutes between two times in the list |
timeFormat |
string |
'auto' |
auto, 12-hour or 24-hour |
minTime |
string|null |
null |
The first time of every day, HH:MM |
maxTime |
string|null |
null |
The last time of every day, HH:MM |
min |
string|DateTimeInterface|null |
null |
The first moment a reader can pick. See Limits |
max |
string|DateTimeInterface|null |
null |
The last moment a reader can pick |
unavailable |
array|string|null |
null |
Days, times and spans a reader cannot pick |
openTo |
string|DateTimeInterface|null |
null |
Where the dialog opens without a value |
months |
int|null |
null |
Months the calendar shows: 1, or 2 with range |
minRange |
int|null |
null |
With range: the fewest days a range may cover, both ends counted |
maxRange |
int|null |
null |
With range: the most days a range may cover, both ends counted |
presets |
bool|string|array|null |
null |
With range: ranges such as "Last 7 days" beside the calendar, as the calendar's presets takes them |
withToday |
bool |
false |
The calendar's today button |
selectableHeader |
bool |
false |
The calendar's month and year selects |
weekNumbers |
bool |
false |
The calendar's week numbers |
weekStartsOn |
int|string |
1 |
The first day of the calendar's week, from 0 to 6, or locale |
fixedWeeks |
bool |
false |
Six rows in every month |
locale |
string|null |
null |
BCP 47 locale for the moment on the trigger, the calendar and the times. null uses your application's locale. See Localization |
scope |
string|null |
null |
Scoped personalization key |
Events
The picker dispatches these on its root, and they bubble:
| Event | When |
|---|---|
wirekit:datetime-picker-open |
The dialog opens |
wirekit:datetime-picker-close |
The dialog closes, however it closes |
wirekit:datetime-picker-clear |
The reader empties the field with the clear button |
The root carries data-open while the dialog is open. The hidden field fires input and change for each moment picked, as a native field does, so wire:model. and wire:model.blur work as on any field.
Accessibility
- The trigger is a button with
aria-haspopup="dialog",aria-expandedandaria-controls. Its name is the field's label and the moment it shows, "Meeting Oct 27, 2026 at 10:30 AM", and "(required)" for a required field. - The dialog is a
role="dialog"witharia-modal="true", named by the field's label and what it is for, "Meeting, Select a date and time". Enter, Space and Down Arrow on the trigger open it and move the focus to the day of the value, or without one to today or the next day that can be picked, on every opening; Tab stays inside it, and Escape closes it and returns the focus to the trigger. - The calendar is the calendar component, with its grid, its keyboard and its names.
- On a phone the dialog is a modal: the page behind it is inert and does not scroll, the focus stays in the dialog, and a tap on the backdrop or Escape closes it and returns the focus to the trigger. The select that stands in for the list of times is named "Time", or by the row of a range's end, and offers the same times, a closed one disabled.
- The list of times is a
listboxnamed "Time". The arrow keys move between open times, Home and End go to the first and the last, Page Up and Page Down an hour, and typing goes to the time typed:9to nine o'clock,14to two in the afternoon,9:3to half past nine. Enter or Space picks. A closed time is passed over and named as unavailable. - A day picked without a time moves the focus to the list of times.
- With
rangethe calendar picks the two days as the calendar does withrange. The time of each end stands in a row named "Start" or "End": a button named by the row and the time it shows, which opens the list of times as alistboxnamed by the row, or withtime="input"the typed time named by the row. Select range is described by the sentence that says the end must come after the start, which stands in a live region (aria-live="polite") and is announced as it appears, as a field's error is, unlessannounce-erroris off. - With
time="input"the segments are spin buttons named "Hour", "Minute" and "AM/PM", in a group named "Time", each with its range and its value. Up and Down change a segment, Left and Right move between them, and Tab goes on to the button at the end of the field, then to Cancel and Select. The button is named "Suggested times" and opens the list of times; Escape closes the list first, and the dialog with the next press. A day picked without a time it can take moves the focus to the first segment. The sentence that says why a typed time is not taken is announced, and describes each segment. - With
type="input"the field is arole="group"named by the label. Its segments are spin buttons named "Day", "Month", "Year", "Hour", "Minute" and "AM/PM", each with its range and its value, and the label points at the first one. The hint and the error describe each segment and the button at the end of the field, which is named "Select a date and time" and opens the dialog; Alt+Down Arrow opens it from a segment, as on a native field. The clear button empties the field and moves the focus to its first segment. The sentence that says why a typed moment is not taken describes each segment and stands in a live region (aria-live="polite"), announced as it appears unlessannounce-erroris off. Withrangethe two fields are groups named by the label and their word, "Stay Start" and "Stay End", in a group named by the label, each with a sentence of its own; the sentence of the end also says when the end does not come after the start or the range covers too few or too many days. The button beside them is named "Select a date and time range".
Keyboard Interaction
| Key | Where | Action |
|---|---|---|
| Enter, Space, Down Arrow | Trigger | Opens the dialog, with the focus on the day of the value, or on today or the next day that can be picked |
| Escape | Dialog | Closes the dialog and returns the focus to the trigger; with the suggestions of a typed time or the list of a range's time open, it closes them first |
| Tab, Shift+Tab | Dialog | Move between the calendar, the times and the buttons; the focus stays in the dialog |
| Arrow keys, Home, End, Page Up, Page Down | Calendar | As on the calendar; with Shift, Page Up and Page Down move a year |
| Up, Down | List of times | Move between the open times |
| Home, End | List of times | The first and the last open time |
| Page Up, Page Down | List of times | An hour back or ahead |
| A digit | List of times | Goes to the time typed: 9 to nine o'clock, 14 to two in the afternoon, 9:3 to half past nine |
| Enter, Space | List of times | Picks the time |
| Enter, Space | Time of a range's end | Opens its list of times, with the focus on the time it shows or the nearest open one |
| Up, Down | Typed time | Steps the segment round its range |
| Left, Right | Typed time | Moves between the segments |
| Home, End | Typed time | The first and the last value of the segment |
a, p |
Typed time | Sets AM or PM |
| Backspace | Typed time | Empties the segment |
| A digit | Typed moment | Sets the segment and moves on once no other digit fits, the year at its fourth digit |
| Up, Down | Typed moment | Steps the segment round its range; an empty year starts at the current one |
| Left, Right | Typed moment | Moves between the segments |
| Home, End | Typed moment | The first and the last value of the segment |
a, p |
Typed moment | Sets AM or PM |
| Backspace | Typed moment | Empties the segment, or moves back from an empty one |
| Alt+Down Arrow | Typed moment | Opens the dialog |
Design Tokens
| Token | Used for |
|---|---|
--size-wk-time-list |
Width of the list of times beside the calendar |
--size-wk-sm / --size-wk-md / --size-wk-lg |
Height of the trigger at each size, and of a time in the list |
--size-wk-xl |
The least height of the list of times |
--color-wk-bg-input / --color-wk-border-strong |
The trigger's surface and border |
--color-wk-bg-elevated / --color-wk-border / --shadow-wk-lg |
The dialog's surface, border and shadow |
--color-wk-bg-hover / --color-wk-bg-active |
A time under the pointer, and the time picked |
--color-wk-text-muted / --color-wk-text-placeholder |
The word "at" and the icons, and the placeholder |
--radius-wk-md / --radius-wk-lg / --radius-wk-sm |
The trigger, the dialog and a time |
--color-wk-accent / --color-wk-accent-fg |
The segment of a typed time that has the focus |
--color-wk-border-error / --color-wk-danger-text |
The field of a typed time that is not taken, and the sentence that says why |
--color-wk-overlay / --z-wk-modal |
The backdrop behind the dialog on a phone, and the layer the two stand on |
--size-wk-modal-sm |
The width of the dialog on a phone, less the edges of a narrower screen |
--size-wk-touch-target |
The size the days of the calendar grow to on a phone |
Personalization
Override classes globally via WireKit:::
use Pushery\WireKit\WireKit;
WireKit::personalize('datetime-picker', [
'trigger' => 'rounded-[var(--radius-wk-lg)]',
'panel' => 'shadow-[var(--shadow-wk-xl)]',
]);