Calendar
The <x-wirekit::calendar> component renders a standalone month grid for single-date selection. It supports full keyboard navigation (arrow keys, Page Up/Down, Home/End) and integrates with forms via a hidden input. The implementation follows the WAI-ARIA Date Picker Dialog pattern.
Usage
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
Multi-month & quick navigation
Render several months side by side with months, or swap the static month label for native month + year jump selects with selectable-header. Both are opt-in, so the default single-month calendar is unchanged.
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
Date range
Add the range flag and the calendar takes two clicks: the first sets the start,
the second the end. A second click before the start simply becomes the start —
there is no rule to learn about which end to pick first. While only the start is
set, the day under the pointer stands in for the end, so the shading answers the
question a reader is actually asking: what am I about to choose.
value reads and writes YYYY-MM-DD/YYYY-MM-DD, the same spelling
date-picker uses with its own
range flag — a value moves between the two without translation. The form
receives name[start] and name[end] alongside the combined name field, so a
handler written for a single date still gets something it understands.
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
In a multi-month view the day grids stay fully keyboard-navigable: arrow keys cross between the displayed grids before shifting the whole window.
Week start
Weeks begin on Monday by default. Pass week-starts-on to change it — 0 for a Sunday-first week, 1 for Monday. The app-wide default lives in config/wirekit.php under components.calendar.week-starts-on.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Pre-selected Date
Pass an ISO 8601 date string (YYYY-MM-DD) to the value prop. The calendar opens on that date's month with the day highlighted.
<x-wirekit::calendar name="start_date" value="2026-01-10" />
No Initial Value
Without a value, the calendar defaults to the current month with today's date visually highlighted (ring indicator, not selected).
<x-wirekit::calendar name="appointment" />
Inside a Form
The calendar renders a hidden <input> with the given name attribute. When a date is selected, the hidden input's value updates and dispatches an input event — making it compatible with standard form submission and wire:model.
<form wire:submit="save">
<x-wirekit::field label="Start Date">
<x-wirekit::calendar name="start_date" wire:model="startDate" />
</x-wirekit::field>
<x-wirekit::button type="submit">Save</x-wirekit::button>
</form>
With Livewire
The hidden input dispatches a native input event on selection, so wire:model bindings work automatically.
<x-wirekit::calendar name="event_date" wire:model.live="eventDate" />
Visual States
| State | Visual Treatment |
|---|---|
| Today | Bold weight + accent ring outline (when not selected) |
| Selected | Accent background with accent foreground text |
| Current month day | Normal text, hover shows subtle background |
| Adjacent month day | Muted text with reduced opacity, not clickable |
Keyboard
The calendar grid supports full keyboard navigation following the WAI-ARIA Grid pattern:
| Key | Action |
|---|---|
| Arrow Right | Move focus to the next day |
| Arrow Left | Move focus to the previous day |
| Arrow Down | Move focus to the same day next week |
| Arrow Up | Move focus to the same day previous week |
| Page Down | Move to the next month |
| Page Up | Move to the previous month |
| Home | Move focus to the first day of the month |
| End | Move focus to the last day of the month |
| Enter / Space | Select the focused date |
When arrow keys move past the first or last day of the month, the calendar navigates to the previous or next month automatically.
Date Format
The calendar uses ISO 8601 (YYYY-MM-DD) format internally and in the hidden input value. The month/year header displays a localized English format (e.g., "April 2026") using Date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }).
European / German Format
The header locale is currently set to en-US. To display the month header in German (e.g., "April 2026" → "April 2026" in German, or "Dezember 2026" for December), you can override the monthLabel getter via Alpine:
<x-wirekit::calendar name="termin" value="2026-04-15"
x-init="
Object.defineProperty($data, 'monthLabel', {
get() {
const d = new Date(this.viewYear, this.viewMonth, 1);
return d.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
}
});
"
/>
This changes the month header to the German locale — "April 2026", "Dezember 2026", "März 2026", etc.
The hidden input always uses ISO 8601 (YYYY-MM-DD) regardless of the display locale. If your backend needs a different format (like DD.MM.YYYY), convert on the server side:
// In your Livewire component or controller
$formatted = Carbon::parse($this->startDate)->format('d.m.Y'); // 15.04.2026
$written = Carbon::parse($this->startDate)->translatedFormat('d. F Y'); // 15. April 2026
The translatedFormat() method from Carbon respects your config('app.locale') setting, so with 'locale' => 'de' it outputs German month names automatically.
Optimistic UI
Pass the name of the Livewire method the calendar should call and the date lands immediately, then confirms or undoes itself when the server answers:
<x-wirekit::calendar
name="due"
: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') }}"></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.
Accepted
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
The date is kept and nothing is said.
Refused
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
The old date comes back, and the refusal is spoken.
Slow to answer
| Mo | Tu | We | Th | Fr | Sa | Su |
|---|---|---|---|---|---|---|
The dashed outline is the provisional state.
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:click. 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 calendar knows where it started.
The displayed month does not roll back with the date. If you paged forward to March and the pick was refused, you are still looking at March — only the selection returns to what the server has. Where you are looking is yours; the value is the server's.
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 calendar sits in a form that already 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 |
|---|---|---|---|
value |
string|null |
null |
Initial selected date in YYYY-MM-DD format |
optimistic |
string|null |
null |
Livewire method to call, showing the new date before the server confirms it. See Optimistic UI. |
optimisticArgs |
array |
[] |
Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |
name |
string |
'date' |
Name attribute for the hidden form input (read from $attributes) |
months |
int |
1 |
Number of consecutive months to display side by side (clamped 1–4) |
range |
bool |
false |
Two-click range selection. value reads and writes YYYY-MM-DD/YYYY-MM-DD, and the form also receives name[start] and name[end] |
selectableHeader |
bool |
false |
Replace the static month label with native month + year jump selects |
weekStartsOn |
int |
1 |
First day of the week — 0 (Sunday) or 1 (Monday, default). App-wide default in config/wirekit.php |
scope |
string|null |
null |
Scoped personalization key |
Accessibility
- Grid:
role="grid"on the<table>element - Cells:
role="gridcell"on each<td> - Rows:
role="row"on each<tr> - Day headers:
<th scope="col">for column headers (Su, Mo, Tu, etc.) - Selected day:
aria-selected="true"on the active day button - Roving tabindex: only the focused day has
tabindex="0"; all others havetabindex="-1" - Month label:
aria-live="polite"announces month changes to screen readers - Nav buttons:
aria-label="Previous month"/aria-label="Next month" - Chevron SVGs:
aria-hidden="true"(decorative) - Disabled days (adjacent months):
disabledattribute prevents interaction selectableHeader: native<select>controls (full keyboard + screen-reader support) withsr-onlylabels and anaria-livemonth announcement- Multi-month (
months > 1): each month is its ownrole="grid"; arrow keys move between the displayed grids, keeping a single rovingtabindex="0"
Day-cell size on touch
Day cells measure 36×36 and the month navigation buttons 28×28. Both clear the 24×24 minimum of WCAG 2.5.8 and both stay under the 44×44 of the AAA criterion 2.5.5 — a deliberate choice, not an oversight.
The reason is that the enlargement would defeat itself. In a seven-column grid at 36px pitch, a 44px hit area reaches 4px into both horizontal neighbors, and the cell painted last takes the taps meant for the other. A day that is slightly small is workable; a day that activates its neighbor is not. Reaching 44 here needs a bigger calendar, which is a layout decision — so if your app needs it, scale the component rather than only its hit areas.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus into the date grid |
ArrowLeft / ArrowRight |
Move between days |
ArrowUp / ArrowDown |
Move between weeks |
PageUp / PageDown |
Move by month (or year with Shift) |
Home / End |
Jump to start / end of the week |
Enter / Space |
Select the focused date |
Pitfalls
-
Don't use calendar for date input. Use
<x-wirekit::date-picker>. The calendar primitive is for displaying a month view — not collecting input. -
It needs 306px, and below that the last column goes over the edge. Seven columns of day cells do not shrink indefinitely: the grid stops at 280px, and with the panel's 12px padding either side plus its 1px border that makes 306px the narrowest container the calendar fits inside cleanly. Measured in a browser across a sweep from 330px down to 200px, not derived from the stylesheet.
What happens below it is worth knowing, because it is quiet. The grid keeps its 280px and starts overrunning the panel's padding; nothing scrolls and nothing throws. Around 293px the seventh column crosses the panel's outer edge, and in a container that does not scroll a reader simply loses Sunday.
On a 360px phone this is comfortable — a full-width block leaves well over 306px. It matters when the calendar sits in something narrower than the viewport: a preview frame, a sidebar, a split pane. Give it 306px or let its container scroll.
Design Tokens
| Token | Used for |
|---|---|
--font-wk-sans |
Calendar font family |
--font-wk-body-weight / --font-wk-heading-weight |
Day / month-title weight |
--text-wk-xs / --text-wk-sm / --text-wk-md |
Weekday header / day / month-title font size |
--color-wk-text |
Day-cell text |
--color-wk-text-muted |
Out-of-month + weekday header text |
--color-wk-accent |
Selected day background |
--color-wk-accent-fg |
Selected day text |
--color-wk-bg-elevated |
Calendar panel background |
--color-wk-bg-subtle |
Day hover background |
--color-wk-border |
Panel border |
--color-wk-ring |
Focus ring |
--ring-wk-width |
Focus ring width |
--border-wk-width |
Border width |
--radius-wk-sm / --radius-wk-lg |
Day-cell + panel border radius |
--shadow-wk-md |
Panel drop shadow |
--padding-wk-x-md / --padding-wk-y-xs / --padding-wk-y-sm |
Cell + nav button padding |
--transition-wk-duration |
Hover / selection transition |
Personalization
Override classes globally via WireKit::personalize():
use Pushery\WireKit\WireKit;
WireKit::personalize('calendar', [
'base' => 'inline-block bg-white border rounded-lg shadow p-4 font-sans',
'header' => 'flex items-center justify-between mb-2',
]);
Further Reading
- WAI-ARIA Date Picker Dialog Example — the reference implementation this component follows
- WAI-ARIA Grid Pattern — keyboard navigation pattern used for the day grid
- MDN:
role="grid" - MDN:
aria-selected