Skip to main content
Copy for LLM

Date Picker

The <x-wirekit::date-picker> 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

Basic Date Picker

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:

Date picker in a 12rem container
<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/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.

Date range

Range Constraints

With Min and Max

Must be in 2024

Error State

Date picker with an error message

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:

A field that discloses the browser's order

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.date-picker.format-hint to true (see Config Defaults). A plain date field takes the same prop, and datetime-local does too.

Want the application's own format instead?

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.js 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.

Optimistic date — accepted, refused, and a slow answer

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.

What a screen reader hears

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" and aria-describedby on error
  • aria-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 Carbon property, or a property that holds a date such as a model attribute with a date cast, 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 a string property 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::calendar> when you need a month grid to LOOK at — it is a different component for a different job.

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/wirekit.php under components.date-picker. 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.m.Y')) and render it as text; an input the browser localizes is not the surface for it.

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store