Skip to main content
Copy for LLM

Event Calendar

A scheduling calendar for displaying events across month, week, and agenda views. The month view is a day grid with event pills and a "+N more" overflow; the week view is an hour-row time grid with overlap-split event blocks and a current-time line; the agenda view is a chronological list grouped by day. Navigation (previous / next / today) and the view switcher recompute the visible window. Clicking an event emits an event-click event you handle in Livewire.

This is the scheduling grid — distinct from calendar, which is a date-picker widget. It is token-themed with no third-party calendar library.

Month View

The default view — a 7-column day grid. Events render as pills; days with more than three events show a "+N more" indicator, which opens the week showing that day and moves the focus there. All-day events and day markers (see Day Markers below) appear as full-width labels at the top of the cell.

A pill carries its start time to the right of the title, formatted in your application's locale — the same one the rest of the calendar is written in, and overridable per calendar with the locale prop. The title truncates to make room, because a clipped time is unreadable while a clipped title is still recognizable. An all-day event shows no time — the cell it sits in is already the date.

There is no prop to turn it off, and that is deliberate: the pill's accessible name has always ended with the time, so a screen-reader user heard "Sprint planning, Monday June 8, 10:00 AM" where a sighted reader saw only "Sprint planning". Making the visible time optional would let that gap be re-created. If the time is not wanted at all, pass the events as allDay — a day cell already says which day it is.

Month view with events, overflow, and a day marker

No events in this range

Week View

The time grid positions each event by its start and duration. Overlapping events split into side-by-side columns; a red line marks the current time on today's column. All-day events and day markers sit in a dedicated band above the hour grid (timed events have an hour position; all-day ones do not).

Week view with back-to-back, overlapping, all-day, and short events

No events in this range

Events That Span Days

An end on a later day than the start puts the event on every day it covers, in all three views. In the week view it runs to the foot of its first day and starts at the top of the next, so a night shift from 22:00 to 02:00 fills both ends of the night. On every day after its first, the event shows no start time, and its accessible name gives the whole span. For an all-day event, an end date is the last day it covers.

Week view with events that span days

No events in this range

Agenda View

A compact chronological list — the best view on mobile and for a quick "what's next" glance.

Agenda view with a day marker

No events in this range

Day Markers

dayMarkers is a day-level dimension, separate from timed events — use it to mark holidays, working exception days, or notes. Each marker carries a date, a label, an optional type, and an optional blocked flag, and renders across all three views: a full-width band in month, a chip in the week all-day band, and its own line in agenda. A long label wraps in every view rather than being cut, so the whole name stays readable in a narrow calendar.

Holidays and blocked days

No events in this range

A blocked day reads as unavailable — a muted surface with a diagonal hatch, plus an "unavailable" suffix in its accessible name, so the state is never conveyed by color or texture alone. WireKit renders the visual and semantic state only; enforce any booking logic in your own application behind a blocked day.

Day-Marker Shape

Key Required Description
date yes YYYY-MM-DD (parsed date-only — no timezone, so it never drifts a day), or a date object, such as a model attribute with a date cast, which is written as the day it names. A date in another form PHP reads, such as 31.12.2026 or a Unix timestamp, is written as its day too
label yes Display name (e.g. Christmas Day)
type no holiday (default) · working · note — drives the tint
blocked no true marks the day unavailable (muted surface + hatch + accessible state)

A marker goes into the page with these four keys and no others.

Week Start

weekStartsOn sets the first column of the month and week grids — 0 for Sunday through 6 for Saturday (1, Monday, is the default). Set it per instance or globally in config/wirekit.php.

Sunday-start month grid

No events in this range

Selecting a day

Two opt-in props make the month grid answer "what happens on this day?":

  • selectable-days turns each day number into a button. Pressing it dispatches wirekit:event-calendar-day-select with the day as YYYY-MM-DD and the ids of its events, so your component can load or open whatever belongs to that day.
  • day-detail also lists the pressed day's events under the grid, and the +N more control opens that list instead of switching to the week view. It implies selectable-days.
Pick a day to see its events

No events on this day

No events in this range

The day number is the button rather than the whole cell, because the cell also holds the event pills, which are buttons of their own. Each day button is named with the full date, and with day-detail it reports whether its day is the one that is open. Pressing the open day again closes the list without dispatching anything: closing is not a selection.

Filtering by category

Give each event a category and set filterable, and the calendar puts one toggle per category above the views. Every category starts shown; pressing a toggle hides its events from every view, so a hidden event takes no pill in the month grid, no +N more, and no column in the week grid. Events without a category belong to no toggle and always stay visible.

Filter the calendar by category

No events in this range

The toggles follow the order in which the categories first appear in your events. A category is its own field rather than the event's intent: the intent is the event's color, and two categories may well share one.

Attendees

Give an event attendees and the agenda, and the day list from day-detail, shows who is coming: up to three faces beside the title, then a count. A face is the attendee's avatar when you pass one, and their initials otherwise.

Agenda with attendees

No events in this range

The faces are decoration. A screen reader hears every attendee as part of the event's name — "Sprint planning, Monday, June 8, 10:00 AM, with Anna Berg, Ben Cole, Carla Diaz, Dan Evans, and Eva Fox" — joined the way the calendar's language joins a list. The month pills leave attendees out, because a pill has no room for them.

Livewire Integration

Pass events from the server and handle clicks (and view changes) in your component:

{{-- 1. Open a detail modal when an event is clicked --}}
<x-wirekit::event-calendar
    :events="$this->events"
    view="month"
    x-on:event-click="$wire.openEvent($event.detail.id)"
    x-on:view-change="$wire.set('view', $event.detail.view)"
/>
// 2. Shape events for the calendar (start/end are ISO 8601)
public function getEventsProperty(): array
{
    return Appointment::query()
        ->whereBetween('starts_at', [now()->startOfMonth(), now()->endOfMonth()])
        ->get()
        ->map(fn ($a) => [
            'id' => $a->id,
            'title' => $a->title,
            'start' => $a->starts_at->toIso8601String(),
            'end' => $a->ends_at?->toIso8601String(),
            'intent' => $a->status === 'confirmed' ? 'success' : 'warning',
        ])
        ->all();
}

// 3. Open the clicked event
public function openEvent(int $id): void
{
    $this->editing = Appointment::find($id);
}
{{-- 4. Or react to a day: the event carries its date and the ids of its events --}}
<x-wirekit::event-calendar
    :events="$this->events"
    selectable-days
    x-on:wirekit:event-calendar-day-select="$wire.showDay($event.detail.date)"
/>

Event Shape

Key Required Description
id yes Unique identifier (emitted on click). An integer id above Number.MAX_SAFE_INTEGER, such as a 64-bit id, is emitted as its digits, so it comes back exactly
title yes Event title
start yes ISO 8601 start datetime, or a YYYY-MM-DD date, which is read as that calendar day wherever the reader is. A date object works too: an all-day event's start is written as the day it names, any other as the instant it holds. A date in another form PHP reads, such as 2026-06-08 10:00 from a column without a cast, 31.12.2026 or a Unix timestamp, is handed to the browser in ISO 8601, a time without a zone as the wall-clock time it names. An event whose start is no date lies on no day and is left out, and text that cannot be read as a date is reported
end no ISO 8601 end (defaults to start + 1 hour). It may fall on a later day, see Events That Span Days; for an all-day event, a YYYY-MM-DD date is the last day it covers. A date object or a date in another form is written as start is, and an end that is no date counts as none
allDay no true renders "All day" in agenda
intent no accent (default) · success · warning · danger · neutral
category no A label to filter by when the calendar is filterable. Events without one are never filtered
attendees no Names, or ['name' => …, 'avatar' => …] entries. The agenda and the day list show up to three as faces with a count, and every name joins the row's accessible name

The calendar writes its events into the page for its script, each with the keys above and no others, and an attendee with its name and avatar. An event or an attendee that is a model carries nothing else of it into the page.

Props

Prop Type Default Description
level int 2 Heading level for the title, 1–6. The default is what it has always rendered — the calendar's period label — so nothing moves unless you set it. Set it to match the surrounding outline: a skipped level is what heading-order reports, and that rule sits in axe's best-practice tag rather than the WCAG tags most suites run, so it is invisible to an axe sweep and visible in Lighthouse.
events array [] Events (see shape above)
dayMarkers array [] Day-level holiday / working / note markers (see shape)
selectableDays bool false Month day numbers become buttons that dispatch wirekit:event-calendar-day-select with { date, eventIds } — see Selecting a day
dayDetail bool false Lists the pressed day's events under the month grid, and +N more opens that list. Implies selectableDays
filterable bool false One toggle per event category above the views — see Filtering by category. Shown only when the events carry two categories or more
view string 'month' month · week · agenda
date string|DateTimeInterface|null null ISO date the calendar opens on (default today). A YYYY-MM-DD date opens on that day in every time zone, a date object on its own day. A string that names no date is reported while you develop and opens on today
weekStartsOn int 1 First column: 0 (Sunday) – 6 (Saturday); 1 (Monday) is the default
locale string|null null BCP-47 tag for every date and time the calendar shows — the month heading, the weekday columns, the hour gutter, the times and the agenda day labels. null uses your application's locale, so the dates speak the language the rest of the page is written in. Set it when a calendar shows a schedule belonging to a different region than the page around it. In another language than the page's, what it shows carries that locale in lang, and the accessible name of every event and day stays in the page's language, as the words in it do
ariaLabel string 'Calendar' Accessible name for the calendar as a whole
weekLabel string|null null Accessible name for the week time grid. Setting it also turns that grid into a screen-reader landmark — see Accessibility
scope string|null null Scoped personalization name

The date the calendar is looking at

The Alpine scope exposes the focused date as focusedDate — a Date, the period the calendar currently shows. It is what the previous/next controls move and what the title is derived from. The calendar renders no slot, so markup placed between its tags is dropped rather than handed that scope. Read the date from your own script instead, through the calendar's id:

<x-wirekit::event-calendar id="team-calendar" :events="$events" />
// 1. Alpine hands back the calendar's scope for its root element.
const calendar = Alpine.$data(document.getElementById('team-calendar'));

// 2. The Date the calendar shows right now.
const year = calendar.focusedDate.getFullYear();
It was called focus before v2.47.0

A scope property named after a browser global is resolved against window whenever the scope is lost — so instead of an error naming this component, the expression quietly hits something else. focus is one of those names; focusedDate is free of that collision. The old spelling was undocumented, so this changes only markup that reached for it anyway.

Accessibility

  • Each event is a focusable button with a full accessible label ("Design review, Wednesday June 10, 2:00 PM"), so the schedule is operable without relying on the event's color.

  • With selectable-days, each day number is a button named with the full date ("Monday, June 8"), and with day-detail it carries aria-pressed for the day that is open. The list under the grid is a group named with that date, and its rows are the agenda's rows, with the same accessible names.

  • The category toggles are buttons with aria-pressed in a group named "Filter by category". Pressed means shown. After each toggle, a polite status announces how many events remain ("Showing 3 of 5 events"), so the effect of a press is heard as well as seen.

  • Attendee faces are aria-hidden. The row's accessible name already lists every attendee, so the faces are the sighted half of the same information rather than a second announcement.

  • The week time grid carries tabindex="0" and a focus ring, so the scrolling grid is keyboard-reachable (WCAG 2.1.1) in every configuration.

  • The week grid becomes a landmark only when you name it with weekLabel. It gets role="region" and that name, and shows up in a screen reader's landmark list. Without one it stays a plain focusable scroller — because role="region" plus a name is a landmark, and two calendars on one page would otherwise be two landmarks announcing the same thing, which is what axe reports as landmark-unique. It is a separate prop from ariaLabel on purpose: that one names the calendar, and a region borrowing the widget's name would announce the part as the whole.

    <x-wirekit::event-calendar view="week" week-label="Sprint 42 schedule" :events="$events" />
    
  • The title is an aria-live="polite" region, so navigating months / weeks is announced.

  • Today carries aria-current="date" in every view — the month cell, the week column header and the agenda day heading. The accent pill and heavier weight are the visual half; without the attribute, "this is today" would reach a screen reader only as a color, which is what WCAG 1.4.1 rules out.

  • The view switcher is a role="radiogroup" of role="radio" buttons with aria-checked + roving tabindex — arrow keys move and select, wrapping at the ends (the buttons own no tabpanels, so tab semantics would be a broken contract). The checked view is tinted and carries an inner edge in the muted text color, at 3:1 or more against the tint in every preset, so it is not marked by color alone.

  • Under forced colors, which paints no tint, the checked view, a shown category and the chosen day are framed in the system's selection color. A hidden category keeps the line through its name, and today its heavier weight.

Keyboard Interaction

Key Action
Tab Move through navigation, the view switcher, the day numbers (with selectable-days), and event blocks
Enter / Space Activate an event or a day, navigate, or switch view
← / → / ↑ / ↓ Move between Month, Week and Agenda while the view switcher has focus
Escape Dismiss an open event tooltip. A modal or a drawer around the calendar stays open

The view switcher is a radio group, not a tab list: it takes ONE tab stop, and the arrow keys move the selection inside it. That is why Tab steps past the whole switcher rather than through its three buttons.

Design Tokens

Element Token
Today highlight --color-wk-accent
Current-time line --color-wk-danger
Grid borders --color-wk-border
Out-of-month days --color-wk-bg-subtle
Event (accent / success / warning / danger) --color-wk-accent / --color-wk-success / --color-wk-warning / --color-wk-danger
Focus ring --color-wk-ring

Customization

Set the default view and week start via config/wirekit.php:

'components' => [
    'event-calendar' => [
        'view' => 'week',
        'week-starts-on' => 0, // Sunday
    ],
],

Usage & Conventions

This build focuses on displaying a schedule. Drag-to-create / move / resize, recurrence (RRULE), the multi-resource view, and ICS export are planned as follow-ups.

Further Reading

Updated in WireKit v2.68.0 (2026-10-16)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store