Components

Calendar

A date field component that allows users to enter and edit dates.

php artisan blatui:add calendar

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.