Skip to content
Bladewell

<x-widget.calendar>

Calendar Blade component for Laravel and Livewire

Events by month, week or day: bookings, appointments, shifts, a team's leave. Multi-day events run as bars, overlapping ones sit side by side, busy days get a "more" list, and on a phone it becomes an agenda. All-day events keep their dates in any timezone; times show in the timezone you give, and ends are exclusive. views adds a switch between views, and the arrows change the period without reloading the page. Pass arrays, or your models with keys. Read-only: events can link to your pages. Works with Livewire.

php artisan bladewell:add calendar
Also adds
Button, Icon, Popover, Tooltip cursor
Needs PHP extension
intl
Works with
Laravel 12 and 13, Livewire 3 and 4, Tailwind CSS 4.1+, PHP 8.3+

Usage

Loading events

Load what's shown overlaps, not only what starts in it: a booking from the 28th to the 3rd belongs on both months. range() gives the same days the calendar draws, from the same ?date=, view, min and max. Only the person's own events, from the query: everything passed to the calendar is in the page.

PHP
Route::get('/bookings', function (Request $request) {
    $timezone = $request->user()->timezone ?? config('app.timezone');
    // With views on the calendar, the view is in the address too; anything else is the month, as the calendar has it.
    $view = in_array($request->query('view'), ['month', 'week', 'day'], true) ? $request->query('view') : 'month';
    [$from, $to] = Calendar::range($request->query('date'), $timezone, $view, min: '2026-01', max: now()->addYear());

    $bookings = $request->user()->bookings()
        ->where('starts_at', '<', $to)
        ->where('ends_at', '>=', $from)
        ->with('room')
        ->get();

    return view('bookings.index', ['bookings' => $bookings, 'timezone' => $timezone]);
})->name('bookings.index');

// bookings/index.blade.php: the same min and max as above. The calendar reads ?view= and ?date= itself, as its links set
// it. keys says where each field is on a Booking, so the models go in as they are: a name, a dotted path, or a closure.
// <x-widget.calendar
//     label="Bookings"
//     :timezone="$timezone"
//     views="month,week,day"
//     :events="$bookings"
//     :keys="[
//         'title' => 'room.name',
//         'start' => 'starts_at',
//         'end' => 'ends_at',
//         'href' => fn ($booking) => route('bookings.show', $booking),
//         'status' => fn ($booking) => $booking->confirmed ? null : 'Awaiting confirmation',
//     ]"
//     min="2026-01"
//     :max="now()->addYear()"
// />

// All-day events are dates: keep them in date columns and pass Y-m-d strings (or the date casts), with the end the day
// after the last day. One stored as a moment has to be turned into its date where it happens first
// ($event->starts_at->setTimezone($timezone)->toDateString()): the calendar turns down an all-day date with a time
// rather than guess its day. Overlap is the same test on dates:
// ->where('starts_on', '<', $to->toDateString())->where('ends_on', '>', $from->toDateString())

Livewire

In a Livewire component, keep the date in a property (#[Url] public string $date = '';, so it stays in the address) and listen for the links with wire:calendar-navigate: the calendar leaves the page where it is, and the render that follows draws the new period. url gives the links a real address too, for opening one in a new tab. Load the events for $date with Calendar::range(), as without Livewire. The today mark stays on the viewer's today through every render.

Blade
<x-widget.calendar
    label="Bookings"
    :timezone="$timezone"
    :events="$this->bookings"
    :date="$date"
    :url="route('bookings.index', ['date' => '{date}'])"
    wire:calendar-navigate="$set('date', $event.detail.date)"
/>

On the server

The calendar can't stop a request that skips it. Here's what to check on your server, and what to keep cheap.

Security

  • Everything passed to the calendar is in the page, whether it shows or not. Load only the events the person may see, filtered in the query (a policy or a scope), never by hiding them in the view, and leave private details out of the title on a calendar others can see. Titles and statuses are escaped, and an href that isn't a path or an http, https, mailto or tel address (such as javascript:) is dropped, so events people enter themselves can be shown as they are.

    Laravel: Authorization · OWASP: Insecure direct object references

Server cost

  • The date comes from the URL, so anyone can ask for any month, week or day. Load only what's shown, with Calendar::range() and an index on the start and end columns, and give min and max where dates outside them mean nothing, so a crawler following Next doesn't walk on for ever.

Examples

Month

A team's calendar, with buttons for month, week and day. They, Today and the arrows are links that set ?view= and ?date=, which the calendar reads back; the script follows them without loading the page again. Kept between last month and two months ahead; the server loads what's shown. In the month, a timed event shows its time, a dot in its colour and its title; one that runs past midnight, or all day, is a bar across its days. An event can link to its page.

Pass $events from your controller.

November 2026

Times in United Kingdom Time

  1. Monday, November 2, 2026

    • 9:30 AM – 11:00 AM

      Sprint planning

  2. Wednesday, November 4, 2026

    • 2:00 PM – 3:00 PM

      Design review

  3. Monday, November 9, 2026

  4. Wednesday, November 11, 2026

    • All day

      Company offsite

      All day, Wed, Nov 11 to Fri, Nov 13

  5. Thursday, November 12, 2026

    • All day

      Company offsite

      All day, Wed, Nov 11 to Fri, Nov 13

  6. Friday, November 13, 2026

    • All day

      Company offsite

      All day, Wed, Nov 11 to Fri, Nov 13

  7. Sunday, November 15, 2026

    • 4:00 PM – 4:30 PM

      Customer call: Weber GmbH Tentative

  8. Wednesday, November 18, 2026

    • 12:00 PM

      Release

  9. Saturday, November 21, 2026

    • From 11:00 PM

      Server maintenance Planned

      Sat, Nov 21, 11:00 PM to Sun, Nov 22, 2:00 AM

  10. Sunday, November 22, 2026

    • Until 2:00 AM

      Server maintenance Planned

      Sat, Nov 21, 11:00 PM to Sun, Nov 22, 2:00 AM

  11. Wednesday, November 25, 2026

    • 3:00 PM – 4:00 PM

      Retro

Show code
Blade
<x-widget.calendar
    label="Team calendar"
    timezone="Europe/London"
    views="month,week,day"
    :events="$events"
    :min="now()->subMonth()"
    :max="now()->addMonths(2)"
/>

Week

A week as hours, opening at 7:00, or earlier when an event starts earlier; the rest of the day scrolls. All-day events, and those of a day or more, run along the top. Events that start together sit side by side; one that starts during another is drawn over the rest of it, set in a little, so both titles can be read. One over midnight is drawn on both days, and a line marks the current time. Where there's no room for seven columns, as on a phone, it's a list of the week's days.

Pass $events from your controller.

Nov 9 – Nov 15, 2026

Times in United Kingdom Time

  1. Monday, November 9, 2026

    • 10:00 AM – 11:30 AM

      Stakeholder meeting

    • 11:00 AM – 11:15 AM

      Online conference

  2. Tuesday, November 10, 2026

    • 11:00 AM – 12:15 PM

      Quick call with Martin

  3. Wednesday, November 11, 2026

    • 9:00 AM – 10:30 AM

      Product team

    • 9:30 AM – 10:15 AM

      Interview: Priya Confirmed

    • 10:00 AM – 10:20 AM

      Coffee

  4. Thursday, November 12, 2026

    • All day

      Team offsite

      All day, Thu, Nov 12 to Fri, Nov 13

    • 9:00 AM – 9:05 AM

      Stand-up

    • From 10:00 PM

      Deploy window Planned

      Thu, Nov 12, 10:00 PM to Fri, Nov 13, 1:30 AM

  5. Friday, November 13, 2026

    • All day

      Team offsite

      All day, Thu, Nov 12 to Fri, Nov 13

    • Until 1:30 AM

      Deploy window Planned

      Thu, Nov 12, 10:00 PM to Fri, Nov 13, 1:30 AM

    • 5:45 AM – 7:10 AM

      Early flight

  6. Saturday, November 14, 2026

    • All day

      Friends visiting

      All day, Sat, Nov 14 to Tue, Nov 17

  7. Sunday, November 15, 2026

    • All day

      Friends visiting

      All day, Sat, Nov 14 to Tue, Nov 17

Show code
Blade
<x-widget.calendar
    label="My week"
    timezone="Europe/London"
    view="week"
    week-start="1"
    :events="$events"
    param="week"
    :min="now()->subWeeks(2)"
    :max="now()->addWeeks(4)"
/>

Day

One day as hours, which fits a phone: an appointment book, a room's bookings. Previous and Next move a day at a time.

Pass $events from your controller.

Thursday, November 12, 2026

Times in New York Time

Thursday, November 12, 2026
All day
  • Clinic closed for training , All day
  1. Room 2: Physiotherapy , 8:00 AM to 9:00 AM
  2. Room 1: Check-up , 8:30 AM to 9:00 AM
  3. Room 1: Vaccination , 9:00 AM to 9:15 AM, Walk-in
  4. Lunch , 12:30 PM to 1:15 PM
  5. Room 2: Follow-up , 2:00 PM to 2:45 PM, Cancelled
Show code
Blade
<x-widget.calendar
    label="Appointments"
    timezone="America/New_York"
    view="day"
    :events="$events"
    param="day"
    :min="now()->subWeek()"
    :max="now()->addWeeks(2)"
/>

Multi day

Events over several days are one bar across them, kept in the same row for the week, with an arrow where one carries on from the week before or into the next. All-day events are dates, so no timezone moves them; an end is the moment or day after the event, so one ending at midnight stays on its own day.

Pass $events from your controller.

November 2026

Times in United Kingdom Time

  1. Sunday, November 1, 2026

    • All day

      Amara on leave

      All day, Thu, Oct 29 to Tue, Nov 3

  2. Monday, November 2, 2026

    • All day

      Amara on leave

      All day, Thu, Oct 29 to Tue, Nov 3

  3. Tuesday, November 3, 2026

    • All day

      Amara on leave

      All day, Thu, Oct 29 to Tue, Nov 3

  4. Friday, November 6, 2026

    • All day

      Laracon EU

      All day, Fri, Nov 6 to Sun, Nov 8

  5. Saturday, November 7, 2026

    • All day

      Laracon EU

      All day, Fri, Nov 6 to Sun, Nov 8

  6. Sunday, November 8, 2026

    • All day

      Laracon EU

      All day, Fri, Nov 6 to Sun, Nov 8

  7. Thursday, November 12, 2026

    • All day

      Stock-take

      All day, Thu, Nov 12 to Thu, Nov 19

  8. Friday, November 13, 2026

    • All day

      Stock-take

      All day, Thu, Nov 12 to Thu, Nov 19

  9. Saturday, November 14, 2026

    • All day

      Stock-take

      All day, Thu, Nov 12 to Thu, Nov 19

    • From 10:00 PM

      Night shift

      Sat, Nov 14, 10:00 PM to Sun, Nov 15, 6:00 AM

  10. Sunday, November 15, 2026

    • All day

      Stock-take

      All day, Thu, Nov 12 to Thu, Nov 19

    • Until 6:00 AM

      Night shift

      Sat, Nov 14, 10:00 PM to Sun, Nov 15, 6:00 AM

  11. Monday, November 16, 2026

    • All day

      Stock-take

      All day, Thu, Nov 12 to Thu, Nov 19

  12. Tuesday, November 17, 2026

    • All day

      Stock-take

      All day, Thu, Nov 12 to Thu, Nov 19

    • From 9:00 PM

      Late show

  13. Wednesday, November 18, 2026

    • All day

      Stock-take

      All day, Thu, Nov 12 to Thu, Nov 19

  14. Thursday, November 19, 2026

    • All day

      Stock-take

      All day, Thu, Nov 12 to Thu, Nov 19

  15. Tuesday, November 24, 2026

    • All day

      Bank holiday

Show code
Blade
<x-widget.calendar label="Warehouse calendar" timezone="Europe/London" :events="$events" :nav="false" />

Busy days

A day with more events than per-day shows the first ones and a "more" button, which opens all of them, in full, with their times and status. view="month" keeps the grid where there's no room for titles, as on a phone: each day with events shows how many, on a button that lists them.

Pass $events from your controller.

November 2026

Times in United Kingdom Time

Sun Mon Tue Wed Thu Fri Sat
Show code
Blade
<x-widget.calendar label="Appointments" timezone="Europe/London" view="month" :events="$events" :per-day="2" :nav="false" />

Agenda

The month as a list of its days with events, each with its times, status and, for one over several days, its whole span. view="auto", the default, shows this instead of the month where there's no room for it, as on a phone.

Pass $events from your controller.

November 2026

Times in New York Time

  1. Tuesday, November 3, 2026

    • 8:30 AM – 9:15 AM

      Dentist

  2. Saturday, November 7, 2026

    • From 6:40 PM

      Flight to Lisbon Booked

      Sat, Nov 7, 6:40 PM to Sun, Nov 8, 7:05 AM

  3. Sunday, November 8, 2026

    • Until 7:05 AM

      Flight to Lisbon Booked

      Sat, Nov 7, 6:40 PM to Sun, Nov 8, 7:05 AM

    • All day

      Lisbon trip

      All day, Sun, Nov 8 to Wed, Nov 11

  4. Monday, November 9, 2026

    • All day

      Lisbon trip

      All day, Sun, Nov 8 to Wed, Nov 11

  5. Tuesday, November 10, 2026

    • All day

      Lisbon trip

      All day, Sun, Nov 8 to Wed, Nov 11

  6. Wednesday, November 11, 2026

    • All day

      Lisbon trip

      All day, Sun, Nov 8 to Wed, Nov 11

  7. Sunday, November 15, 2026

    • 9:00 AM

      Rent due

  8. Friday, November 20, 2026

    • 7:00 PM – 9:00 PM

      Book club Tentative

Show code
Blade
<x-widget.calendar label="My plans" timezone="America/New_York" view="agenda" :events="$events" :nav="false" />

Props

Other attributes, such as autocomplete or data-*, are passed through to the element. class styles the component's outer wrapper.

<x-widget.calendar>

Prop Default Description
label null Required: names the calendar for screen readers ("Team calendar", "Bookings").
id null The calendar's id; scripts can reach it by it, so it must be unique. Made up when left out.
timezone null Required: the timezone timed events are shown in, and named in, e.g. Europe/London or the signed-in person's. All-day events are dates and never move between days, whatever it is.
events [] The events: arrays, objects or models with id, title, start, and optionally end (exclusive), all_day, href, status (text, e.g. "Tentative") and tone (primary, success, warning, error or neutral). Pass every event that overlaps what's shown, including those that began before it: Bladewell\Support\Calendar::range() gives those moments.
keys [] Where to read a field when your events name it differently, so models can be passed as they are: the field => its key, a dotted path, or a closure given the event. ['title' => 'room.name', 'start' => 'starts_at', 'end' => 'ends_at', 'href' => fn ($booking) => route('bookings.show', $booking)].
date null What's shown: YYYY-MM, YYYY-MM-DD or a date object; the month, week or day holding it. Left out, it's read from the page's query (param), where the links put it, so the links work as they are. Anything that isn't a date, such as a mistyped ?date=, shows today's.
view 'auto' month; week or day, as hours; agenda, a list of the month's days with events; or auto: the month where there's room, the agenda where there isn't, as on a phone. week, too, is a list of its days where there's no room, and month keeps its grid there, each day showing how many events it has, on a button that lists them.
views null The views people can switch between, on buttons beside Today: any of month, week and day, as an array or "month,week,day". The one showing is read from the page's query (view-param), else it's view, else the first.
view-param 'view' The query parameter the view buttons set.
per-day 3 The most events a day shows in the month, or along the top of a week, before the rest go behind its "more" button.
nav true Shows previous, today and next links. false leaves them out, for a calendar you move yourself.
param 'date' The query parameter the links set: YYYY-MM for a month, YYYY-MM-DD for a week (its first day) or a day. The rest of the page's query is kept.
url null A link of your own for each period instead, with {date} where the date goes, and with views, {view} where the view goes: "/bookings/{view}/{date}". Use it inside a Livewire component, where the page's own address isn't the request's.
reload false true: each link loads its whole page, for a page where something besides the calendar changes with the date (a total, a list). Otherwise the script fetches that page and takes only the calendar from it.
min null The earliest date the links go to: YYYY-MM or YYYY-MM-DD, or a date object. Anything earlier shows this.
max null The latest date the links go to; a month means its last day. Anything later shows this.
week-start null The first day of the week, 0 (Sunday) to 6 (Saturday); defaults to the locale's.
locale null The language for month and day names, dates and times (en, fr, ar…); defaults to the app's.
empty null Shown in the agenda when there are no events; "No events this month." or "No events this week." by default.

<x-widget.calendar.entry>

Prop Default Description
event Required One row of a day's list, in the agenda and in a day's "more" list, drawn by the calendar widget. The event.
day Required The day the row is listed under: an event over several days shows its time on that day.
calendar Required The calendar's month, which writes the times the locale's way.

<x-widget.calendar.lanes>

Prop Default Description
day Required One day's bars and dots, drawn by the calendar widget in a month's day and along the top of a week or a day. The day.
calendar Required The calendar it's part of, which writes the times the locale's way.
calendar-id Required The calendar's id, for the day's "more" list.
compact false Where there's no room for bars and titles, as on a phone, shows a count of the day's events instead, on a button that lists them.

Accessibility

All 6 examples above are checked with axe-core against the WCAG 2.2 A and AA rules, in the light theme and the dark one, both as the page draws and with each popover, dialog, toast and tooltip opened, on every change. A change that fails can't be merged. Where axe can't decide, such as contrast on SVG text, the test measures the colours itself instead of letting it pass.

Automated checks can't judge everything: how it sounds in a screen reader, and how it feels to use from the keyboard, still need a person. Check those on your own pages too.

Source

What bladewell:add calendar writes to your app with the default namespaces. Prefer to copy by hand? Take these files, plus the ones from the components it also adds , and the theme and base CSS.

resources/views/components/widget/calendar/entry.blade.php Show
entry.blade.php
resources/views/components/widget/calendar/index.blade.php Show
index.blade.php
resources/views/components/widget/calendar/lanes.blade.php Show
lanes.blade.php
resources/js/widget/calendar/index.js Show
index.js
app/View/Widget/Calendar.php Show
Calendar.php
app/View/Widget/CalendarBlock.php Show
CalendarBlock.php
app/View/Widget/CalendarDay.php Show
CalendarDay.php
app/View/Widget/CalendarEvent.php Show
CalendarEvent.php
app/View/Widget/CalendarMonth.php Show
CalendarMonth.php
app/View/Widget/CalendarWeek.php Show
CalendarWeek.php
app/View/Widget/ElementIds.php Show
ElementIds.php
app/View/Widget/LocalDate.php Show
LocalDate.php