---
title: Date Picker
description: Calendar-based date input
visibility: guest
draft: false
---

# 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

:::preview{title="Basic Date Picker"}
<x-wirekit::date-picker name="birthdate" value="1990-01-01" />
:::

## 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:

:::preview{title="Date picker in a 12rem container"}
<div style="max-width: 12rem;">
    <x-wirekit::date-picker name="start_date_narrow" />
</div>
:::

:::source{language="blade"}
<x-wirekit::date-picker name="start_date_narrow" />
:::

```blade
<div class="max-w-[12rem]">
    <x-wirekit::date-picker label="Start date" name="start_date" />
</div>
```

See [Input — Width](/components/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.

:::preview{title="Date range"}
<x-wirekit::date-picker label="Trip dates" name="trip" range value="2025-06-10/2025-06-17" />
:::

## Range Constraints

:::preview{title="With Min and Max"}
<x-wirekit::date-picker name="event" min="2024-01-01" max="2024-12-31" hint="Must be in 2024" />
:::

## Error State

:::preview{title="Date picker with an error message"}
<x-wirekit::date-picker label="Date of birth" name="birthdate-error-demo" error="Please enter a valid date" />
:::

## Why Native?

The native `<input type="date">` gives us:

- **Zero dependencies** — no JavaScript calendar library to ship
- **Automatic localization** — calendar renders in the user'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.

## 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:

```blade
<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:

```blade
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}"></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.

:::preview{title="Optimistic date — accepted, refused, and a slow answer"}
<livewire:demos.optimistic-host>
<x-wirekit::stack gap="lg" style="max-width: 26rem;">
    <x-wirekit::date-picker name="opt-accept" label="Accepted — the confirmation is silent" value="2026-08-14" optimistic="demoAccept" />
    <x-wirekit::date-picker name="opt-reject" label="Refused — the old date comes back, and the refusal is spoken" value="2026-08-14" optimistic="demoReject" />
    <x-wirekit::date-picker name="opt-slow" label="Slow answer — the dashed outline is the provisional state" value="2026-08-14" optimistic="demoSlow" />
</x-wirekit::stack>
</livewire:demos.optimistic-host>
:::

:::source{language="blade"}
{{-- In your app there is no host: your own Livewire component owns the method. --}}
<x-wirekit::date-picker
    name="due"
    label="Due date"
    :value="$due"
    optimistic="saveDue"
/>
:::

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.

::: info 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` | auto-generated | Element id |
| `value` | `string\|array\|null` | `null` | Initial date `YYYY-MM-DD`; in range mode an array `['start' => .., 'end' => ..]` 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](#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\|null` | `null` | Earliest selectable date (`YYYY-MM-DD`) |
| `max` | `string\|null` | `null` | Latest selectable date (`YYYY-MM-DD`) |
| `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 |
| `error` | `string\|null` | `null` | Error message (also reads from `$errors`) |
| `scope` | `string\|null` | `null` | Scoped personalization key |

## 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 matching `YYYY-MM-DD` (or the configured format) | Set the value directly |
| `ArrowUp` / `ArrowDown` (in supporting browsers) | Step the focused field by ±1 |

## Pitfalls

- **Don't pass a `Carbon\Carbon` instance directly.** The underlying `<input type="date">` expects an ISO string. Use `->format('Y-m-d')` before binding, or rely on Livewire's automatic Carbon ↔ string casting.
- **Don't disable the dropdown.** deliberately ships an HTML5 native picker for accessibility — disabling it sacrifices keyboard-only date entry that screen-reader users depend on.

## 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` / `--padding-wk-y-xs` | 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:

```php
'components' => [
    'date-picker' => ['size' => 'md', 'format' => 'Y-m-d'],
],
```

## Further Reading

- [MDN: `<input type="date">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date)
- [Can I Use: `input type=date`](https://caniuse.com/input-datetime) — browser support matrix
- [MDN: Date and time input types](https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types#date_and_time_pickers)
- [WAI-ARIA: Date Picker Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/) — for custom calendar UIs (not used here)
