Date Picker
The <x-wirekit:: component is a styled wrapper around the native <input type="date">. It uses the browser's built-in calendar popup for zero dependencies and automatic localization to the user's OS locale.
Usage
Width
Like all WireKit form components, the date picker fills its container (w-full). Date fields are typically narrow — constrain the width via the parent element:
<div class="max-w-[12rem]">
<x-wirekit::date-picker label="Start date" name="start_date" />
</div>
See Input — Width for more layout examples (grid columns, mixed widths).
Date Range
Add the range flag to render a linked start + end pair. Each field is a native
date input, and the two are reactively linked — the end can't be set before the
start, and the start can't be set after the end. They submit as name[start]
and name[end]. Pass an initial range as an array or a YYYY-MM-DD/
string. A date object works for either end, and for value, min and max
outside a range: the field reads the day it names.
A range binds with wire:model, which reaches the two ends as {property}.start and
{property}.end. An x-model binds a single date but not a range, which drops it: see
An x-model on a WireKit component.
Range Constraints
Must be in 2024
Error State
Please enter a valid date
Why Native?
The native <input type="date"> gives us:
- Zero dependencies — no JavaScript calendar library to ship
- Automatic localization — the calendar renders in the reader's OS language and date format
- Full accessibility — arrow keys, PageUp/PageDown for months, Home/End for week boundaries, all work out of the box
- Mobile keyboards — native date wheel on iOS, spinner on Android
- Browser-native parsing — handles timezone and format edge cases
The value is always submitted in YYYY-MM-DD format regardless of the display format.
The order is the browser's, and it is not yours to set
That second bullet is a benefit and a trap in the same sentence. A native date field takes its order from the browser's user-interface language, never from the language of the page around it — and no author can change that. It is not an authorable property in any shipped browser.
So a German page read on an English laptop asks for mm/dd/yyyy while every word beside it is
German, and the misreading is silent: 03.04.2026 and 04/03/2026 are both valid dates.
Nothing is rejected, no error appears, and the record carries a different day than the person
meant. On a birth date, a delivery date or a cut-off in a report, that is real damage.
format-hint says so, in the language of the page, with a worked example:
It renders nothing when the browser and the page write dates in the same order, which is the overwhelmingly common case — you will most likely see nothing above. The sentence is composed in the browser, because only the browser knows its own order; its wording comes from your application's language.
To turn it on for every date picker, set components. to true (see
Config Defaults). A plain date field
takes the same prop, and datetime-local does too.
That means giving up the native field, and the cost is the four bullets above: the keyboard model, the calendar the browser opens, the date wheel on a phone, and browser-native parsing. calendar is the component that makes that trade — it takes the application locale and draws the dates itself. Reach for it when the format matters more than the affordances, and keep this one when it does not.
Optimistic UI
Pass the name of the Livewire method the picker should call and the date lands immediately, then confirms or undoes itself when the server answers:
<x-wirekit::date-picker
name="due"
label="Due date"
:value="$due"
optimistic="saveDue"
/>
Load wirekit-optimistic. alongside whichever bundle you already use — below it, in your layout:
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}" defer></script>
Try it
The demo below runs the real path: the change shows immediately, the outline says it is provisional, and the server's answer either confirms it silently or takes it back.
The <livewire:demos.…> wrapper above exists only on this site — it supplies the demo
methods so the page can show a real round trip. The block under it is what you write.
The prop takes a method name rather than true because the component cannot know the action otherwise: server actions reach a WireKit component through the attribute bag, so it never sees your wire:change. optimistic replaces wire:model.live here rather than joining it — the method you name is what changes the value on the server; pass the current one with :value so the picker knows where it started.
range mode is not covered. A range is two values, so an undo would have to answer what it restores when only one end moved — and the two inputs constrain each other, so rolling one back can leave the other holding a bound that no longer applies. Passing both props gives you the plain range picker rather than a layer applied to half of it.
Picking a date announces once, hedged — "Saving" — so the new value is audible as provisional. Confirmation is silent: what was announced is what happened. Only a deviation speaks a second time, which is what makes an undo recognizable as an undo.
Where the picker shows a validation message, the undo stays silent and leaves that message to speak: it tells you what to do, "could not save" does not.
An aborted request announces nothing at all — nothing was refused.
Focus stays exactly where you put it. An undo arrives on the server's schedule, and moving focus then would take you out of your place for a reason you could not predict.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string|null |
null |
Form field name (range mode submits name[start] / name[end]) |
id |
string|null |
derived | Element id. Without one it is derived from name, then from the bound wire:model property (wire:model="due" gives wk-date-due), and counted in page order when there is neither. A second date picker with the same name or property on the page gets -2, so each label and hint names its own field. It is the same on every render, so a Livewire round trip keeps the input a reader is typing into. A <label for> of yours with this id names the field, and then no fallback name stands over it |
value |
string|DateTimeInterface|array|null |
null |
Initial date YYYY-MM-DD or a date object; in range mode an array ['start' => .., 'end' => ..] (each end a string or a date object) or a YYYY-MM-DD/YYYY-MM-DD string |
optimistic |
string|null |
null |
Livewire method to call, showing the new date before the server confirms it. Single-date mode only; ignored with range or disabled. See Optimistic UI. |
optimisticArgs |
array |
[] |
Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |
range |
bool |
false |
When true, renders a linked start + end date pair |
min |
string|DateTimeInterface|null |
null |
Earliest selectable date (YYYY-MM-DD or a date object) |
max |
string|DateTimeInterface|null |
null |
Latest selectable date (YYYY-MM-DD or a date object) |
size |
string |
'md' |
'sm', 'md', 'lg' |
disabled |
bool |
false |
Disabled state |
required |
bool |
false |
Required field |
placeholder |
string|null |
null |
Placeholder (browser-dependent) |
hint |
string|null |
null |
Helper text below input |
help |
string|null |
null |
An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label |
formatHint |
bool |
false |
Say so when the browser writes dates in a different order than the page. Renders nothing when they agree. It does not change the order — see The order is the browser's. The default comes from components.date-picker.format-hint |
label |
string|null |
null |
Visible label above the input. Supplies the accessible name; without it, pass aria-label |
announceError |
bool|null | null |
Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region. |
error |
string|null |
null |
Error message (also reads from $errors) |
scope |
string|null |
null |
Scoped personalization key |
Slots
| Slot | Description |
|---|---|
labelAfter |
Markup after the label and its help question mark, in the same row and outside both, such as a badge that says a setting is not decided yet. See A Mark After the Label. |
Accessibility
- Native
<input type="date">— full browser a11y and keyboard support aria-invalid="true"andaria-describedbyon erroraria-required="true"when required- Focus ring and border color tokens adapt to light/dark mode automatically
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus into the date input |
| Type characters into the focused segment | Set the value directly. The segment order the browser presents is the viewer's locale, not a format the page configures |
ArrowUp / ArrowDown (in supporting browsers) |
Step the focused field by ±1 |
Pitfalls
- Don't bind a date object. Livewire sends a
Carbonproperty, or a property that holds a date such as a model attribute with adatecast, to the browser as a full timestamp (2026-10-05T00:00:00+00:00). The underlying<input type="date">drops a value that is not a date, so the field stays empty, and after a choice the server's answer empties it again. Bind astringproperty that holds->format('Y-m-d')and convert it when you save. In debug mode the component logs a warning for a binding to a date object. - Don't replace the native picker with a custom calendar. This component deliberately wraps
<input type="date">, which brings the platform's own keyboard model and screen-reader announcements with it. There is no prop that turns that off, and rebuilding it in markup gives up date entry that keyboard-only and screen-reader users rely on. Reach for<x-wirekit::when you need a month grid to LOOK at — it is a different component for a different job.calendar>
Design Tokens
| Token | Used for |
|---|---|
--text-wk-xs / --text-wk-sm / --text-wk-md / --text-wk-lg |
Font size per size prop |
--color-wk-text |
Selected date text |
--color-wk-text-muted |
Calendar icon + hint |
--color-wk-text-placeholder |
Empty-state text |
--color-wk-bg-input |
Input background |
--color-wk-accent |
Selected date highlight |
--color-wk-border-strong |
Default border |
--color-wk-border-error |
Error-state border |
--color-wk-danger-text |
Error message |
--color-wk-ring |
Focus ring |
--ring-wk-width |
Focus ring width |
--border-wk-width |
Border width |
--radius-wk-md |
Border radius |
--size-wk-sm / --size-wk-md / --size-wk-lg |
Input height per size prop |
--padding-wk-x-md |
Input padding |
--opacity-wk-disabled |
Disabled visual weight |
--transition-wk-duration |
Hover / focus transition |
Config Defaults
The defaults live in config/ under components.. Override them globally:
'components' => [
'date-picker' => ['size' => 'md', 'format-hint' => false],
],
format-hint set to true turns the order hint on for every date picker, and a picker that passes format-hint itself keeps its own value. The absence worth naming is format. A native <input type="date"> displays the date in the viewer's locale by specification — the page does not get a say, so there is nothing for a format option to do. The value the form receives is YYYY-MM-DD whatever the viewer sees. To present a date in a format you choose, format it on the server (->format('d.) and render it as text; an input the browser localizes is not the surface for it.
Further Reading
- MDN:
<input type="date"> - Can I Use:
input type=date— browser support matrix - MDN: Date and time input types
- WAI-ARIA: Date Picker Dialog Pattern — for custom calendar UIs (not used here)