Components
Calendar
A date field component that allows users to enter and edit dates.
Since 1.20, an incoming calendar:set / calendar:set-range / calendar:today no longer emits calendar-change. That event now means "the user picked a day" and nothing else, so close the popover when the selection is complete can be written literally — a calendar that seeds itself on open no longer closes on the click that opened it. To observe programmatic changes too, listen for calendar:updated ({ id, mode, value, source }) and branch on source. Any re-entrancy flag you were carrying can go.
Driving a calendar from outside? Prefer x-model (the root exposes x-modelable="value") over re-seeding it on every open, and give each instance a calendar-id so a window-level calendar:* event can target one calendar instead of every calendar on the page.
<x-ui.calendar mode="single" value="2025-06-12" default-month="2025-06-12" class="rounded-md border shadow-sm" />
Controlled
Selected:
{{--
Controlled calendar in a popover — the pattern a search bar needs: two triggers sharing
ONE calendar, a value that is seeded up front, and quick-picks that push a range in.
The two contracts that make this work without a re-entrancy flag:
• x-model on the calendar keeps `stay` and the selection entangled both ways, so the
popover never has to be re-seeded on open;
• only a real day click emits `calendar-change`, so "close when the range is complete"
can be written literally — seeding it (here, the quick-picks) leaves it open.
A quick-pick is aimed at THIS calendar by id, so a second picker on the page is untouched.
--}}
<div
x-data="{
open: false,
field: 'from',
stay: { from: '{{ now()->addDays(7)->format('Y-m-d') }}', to: '{{ now()->addDays(12)->format('Y-m-d') }}' },
fmt(d) { return d ? new Date(d + 'T00:00:00').toLocaleDateString(undefined, { day: 'numeric', month: 'short' }) : '—'; },
show(which) { this.field = which; this.open = true; },
}"
{{-- Read the value off the event, not off `stay`: the entangled binding settles on
Alpine's next tick, the event detail is authoritative right now. --}}
@calendar:updated="$event.detail.source === 'select'
&& $event.detail.value.from && $event.detail.value.to && (open = false)"
class="relative flex w-full max-w-md flex-col gap-3"
>
<div class="border-input flex items-stretch rounded-md border shadow-xs">
<button type="button" @click="show('from')" :data-active="open && field === 'from'"
class="data-[active=true]:bg-accent flex-1 rounded-s-md px-3 py-2 text-start text-sm transition-colors">
<span class="text-muted-foreground block text-xs">{{ __('Check in') }}</span>
<span class="font-medium" x-text="fmt(stay.from)"></span>
</button>
<button type="button" @click="show('to')" :data-active="open && field === 'to'"
class="data-[active=true]:bg-accent flex-1 rounded-e-md border-s px-3 py-2 text-start text-sm transition-colors">
<span class="text-muted-foreground block text-xs">{{ __('Check out') }}</span>
<span class="font-medium" x-text="fmt(stay.to)"></span>
</button>
</div>
<div x-show="open" x-cloak @click.outside="open = false" @keydown.escape.window="open = false"
class="bg-popover text-popover-foreground z-50 w-fit rounded-md border p-0 shadow-md">
{{-- Quick-picks dispatch at one instance by id, so other calendars on the page ignore them. --}}
<div class="flex flex-wrap gap-2 border-b p-2">
@foreach ([__('This weekend') => [5, 7], __('Next week') => [7, 14]] as $label => $offsets)
<x-ui.button variant="outline" size="sm"
x-on:click="$dispatch('calendar:set-range', {
id: 'stay',
from: '{{ now()->addDays($offsets[0])->format('Y-m-d') }}',
to: '{{ now()->addDays($offsets[1])->format('Y-m-d') }}',
})">{{ $label }}</x-ui.button>
@endforeach
<x-ui.button variant="ghost" size="sm" x-on:click="$dispatch('calendar:clear', { id: 'stay' })">
{{ __('Clear') }}
</x-ui.button>
</div>
<x-ui.calendar
calendar-id="stay"
mode="range"
x-model="stay"
week-start="monday"
:number-of-months="2"
:min-date="now()->format('Y-m-d')"
:show-outside-days="false"
class="border-0"
/>
</div>
<p class="text-muted-foreground text-xs">
{{ __('Selected') }}: <span x-text="fmt(stay.from) + ' → ' + fmt(stay.to)"></span>
</p>
</div>
Disabled Dates
{{-- Disable everything before June 12, 2025 --}}
<x-ui.calendar
mode="single"
value="2025-06-12"
default-month="2025-06-12"
:disabled="['before' => '2025-06-12']"
class="rounded-md border shadow-sm"
/>
Disabled Weekends
{{-- dayOfWeek: 0 = Sunday, 6 = Saturday --}}
<x-ui.calendar
mode="single"
default-month="2025-06-01"
:disabled="['dayOfWeek' => [0, 6]]"
class="rounded-md border shadow-sm"
/>
Dropdown Caption
<x-ui.calendar
mode="single"
value="2025-06-12"
default-month="2025-06-12"
caption-layout="dropdown"
class="rounded-md border shadow-sm"
/>
Min Max Range
{{-- Require a range of at least 2 and at most 7 nights --}}
<x-ui.calendar
mode="range"
:value="['from' => '2025-06-12', 'to' => '2025-06-16']"
default-month="2025-06-12"
:min="2"
:max="7"
class="rounded-md border shadow-sm"
/>
Multiple
<x-ui.calendar
mode="multiple"
:value="['2025-06-05', '2025-06-12', '2025-06-18']"
default-month="2025-06-12"
:max="5"
class="rounded-md border shadow-sm"
/>
Multiple Months
<x-ui.calendar
mode="single"
value="2025-06-12"
default-month="2025-06-12"
:number-of-months="2"
class="rounded-md border shadow-sm"
/>
Outline Days
{{-- Outlined day cells via the `button-variant` prop. --}}
<x-ui.calendar
mode="single"
value="2025-06-12"
default-month="2025-06-12"
button-variant="outline"
class="rounded-md border shadow-sm"
/>
Range
<x-ui.calendar
mode="range"
:value="['from' => '2025-06-09', 'to' => '2025-06-26']"
default-month="2025-06-09"
:number-of-months="2"
class="rounded-md border shadow-sm"
/>
Week Numbers
<x-ui.calendar
mode="single"
value="2025-06-12"
default-month="2025-06-12"
:show-week-number="true"
class="rounded-md border shadow-sm"
/>
Week Start Monday
{{-- week-start: 0 = Sunday (default), 1 = Monday. Independent of locale. --}}
<x-ui.calendar
mode="single"
value="2025-06-12"
default-month="2025-06-12"
:week-start="1"
class="rounded-md border shadow-sm"
/>
API Reference
Props, slots and exposed methods for
<x-ui.calendar>.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
mode |
string
|
'single'
|
Selection behaviour: one date, a set of dates, or a start/end range.
single
multiple
range
|
value |
string|array
|
— | The initial selection. A "Y-m-d" string for single mode, an array of "Y-m-d" strings for multiple, or ["from"=>, "to"=>] for range. |
name |
string
|
— | When set, renders hidden inputs with this name so the selection submits with a form. Range mode emits name[from] and name[to]. |
numberOfMonths |
int
|
1
|
How many month grids to render side by side. |
defaultMonth |
string
|
— | The "Y-m-d" date whose month is shown first when there is no value. |
weekStart |
int|string
|
0
|
First day of the week: 0–6 (0 = Sunday) or a day name like "monday". |
captionLayout |
string
|
'label'
|
Render the month/year caption as plain text or as selectable dropdowns.
label
dropdown
|
showWeekNumber |
bool
|
false
|
Show an ISO week-number column on the left of each grid. |
showOutsideDays |
bool
|
true
|
Render days from adjacent months that pad the first and last week. |
locale |
string
|
— | BCP-47 locale (e.g. "en-GB") for month names, weekday labels, and date formatting. |
disabled |
array
|
— | Matcher(s) describing which dates cannot be selected. See Disabled / modifier matchers below. |
minDate |
string
|
— | Earliest selectable date ("Y-m-d"). Behaviour with later dates is governed by outOfRange. |
maxDate |
string
|
— | Latest selectable date ("Y-m-d"). Behaviour with earlier dates is governed by outOfRange. |
outOfRange |
string
|
'disable'
|
How to treat dates outside minDate/maxDate: prevent selection, or allow it but flag the day in red.
disable
flag
|
min |
int
|
— | Range mode only: minimum range length in days. Alias of minDays. |
max |
int
|
— | Range mode only: maximum range length in days. Alias of maxDays. In multiple mode it caps the number of selectable dates. |
minDays |
int
|
— | Range mode only: minimum range length in days (clearer name for min). |
maxDays |
int
|
— | Range mode only: maximum range length in days (clearer name for max). |
startMonth |
string
|
— | Earliest navigable month ("Y-m-d"); the previous-month button stops here. |
endMonth |
string
|
— | Latest navigable month ("Y-m-d"); the next-month button stops here. |
disableNavigation |
bool
|
false
|
Hide/disable the previous and next month controls. |
buttonVariant |
string
|
'ghost'
|
Visual style of the month navigation buttons.
ghost
outline
|
required |
bool
|
false
|
Prevent clearing the selection by clicking the currently selected day. |
modifiers |
array
|
— | Map of modifier name to a matcher (same shape as disabled). Tags matching days so they can be styled. |
modifiersClass |
[modifier => class]
|
— | Map of modifier name to the Tailwind classes applied to days that match that modifier. |
calendarId |
string
|
— | Instance handle. Aims the calendar:* hooks at this calendar when they are broadcast on window, and stamps outgoing calendar:updated events. Defaults to the element id. |
prevMonthLabel |
string
|
__('Go to the previous month')
|
Accessible name of the previous-month button. |
nextMonthLabel |
string
|
__('Go to the next month')
|
Accessible name of the next-month button. |
todayLabel |
string
|
__('Today, :date')
|
aria-label used for today. The :date placeholder is replaced by the date formatted in the calendar locale. |
selectedLabel |
string
|
__('selected')
|
Appended to a day aria-label when that day is part of the selection. |
Disabled / modifier matchers
| Prop | Type | Default | Description |
|---|---|---|---|
before |
string
|
— | Match every date before this "Y-m-d" date. |
after |
string
|
— | Match every date after this "Y-m-d" date. |
dayOfWeek |
array
|
— | Match these weekdays (0 = Sunday … 6 = Saturday), e.g. [0, 6] for weekends. |
from |
string
|
— | Start of an inclusive ["Y-m-d", "Y-m-d"] date range to match (paired with to). |
to |
string
|
— | End of an inclusive date range to match (paired with from). |
Events
Events the component listens for (in) and dispatches (out). Both directions bubble,
so .window works
even when the component is teleported.
| Event | Direction | Detail |
|---|---|---|
calendar-change |
Out |
'Y-m-d' | ['Y-m-d', …] | { from, to }
The user picked a day. Programmatic changes never fire it, so "close the popover when the selection is complete" needs no re-entrancy flag.
|
calendar:updated |
Out |
{ id, mode, value, source }
Any change. source is select (a user pick), set, set-range, today, clear or value (a controlled write). value has the same shape as the value prop. When the calendar sits in a teleported popover the event bubbles to window, not to the trigger's ancestors — listen inside the popover, or on .window and match detail.id.
|
calendar:set |
In |
'Y-m-d' | Date | { date, id? }
Selects a date. Single mode only — ignored, view included, in the other modes.
|
calendar:set-range |
In |
{ from, to, id? }
Selects a range (either end may be null). Range mode only.
|
calendar:today |
In |
{ id? }
Selects today. Single mode only.
|
calendar:goto |
In |
'Y-m' | 'Y-m-d' | Date | { month, id? }
Scrolls the visible month(s) without selecting anything. Works in every mode.
|
calendar:clear |
In |
{ id? }
Empties the selection. Works in every mode.
|