---
title: Event Calendar
description: Scheduling calendar with month, week, and agenda views
visibility: guest
draft: false
---

# 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`](/components/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. All-day events and day markers
(see [Day Markers](#day-markers) below) appear as full-width labels at the top
of the cell.

:::preview{title="Month view with events, overflow, and a day marker"}
<x-wirekit::event-calendar
    view="month"
    date="2026-06-15"
    aria-label="Team schedule"
    :day-markers="[
        ['date' => '2026-06-19', 'label' => 'Company holiday', 'type' => 'holiday'],
        ['date' => '2026-06-25', 'label' => 'Office closed', 'type' => 'holiday', 'blocked' => true],
    ]"
    :events="[
        ['id' => 1, 'title' => 'Sprint planning', 'start' => '2026-06-08T10:00:00', 'end' => '2026-06-08T11:00:00', 'intent' => 'accent'],
        ['id' => 2, 'title' => 'Design review', 'start' => '2026-06-10T14:00:00', 'end' => '2026-06-10T15:00:00', 'intent' => 'success'],
        ['id' => 3, 'title' => 'Standup', 'start' => '2026-06-12T09:00:00', 'end' => '2026-06-12T09:15:00', 'intent' => 'accent'],
        ['id' => 4, 'title' => 'Release 2.6', 'start' => '2026-06-12T11:00:00', 'end' => '2026-06-12T12:00:00', 'intent' => 'danger'],
        ['id' => 5, 'title' => 'Code review', 'start' => '2026-06-12T14:00:00', 'end' => '2026-06-12T15:00:00', 'intent' => 'success'],
        ['id' => 6, 'title' => 'Retro', 'start' => '2026-06-12T16:00:00', 'end' => '2026-06-12T17:00:00', 'intent' => 'warning'],
        ['id' => 7, 'title' => 'All-hands', 'start' => '2026-06-18T09:00:00', 'end' => '2026-06-18T10:00:00', 'intent' => 'warning'],
        ['id' => 8, 'title' => 'Team offsite', 'start' => '2026-06-22T09:00:00', 'allDay' => true, 'intent' => 'success'],
    ]"
/>
:::

## 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).

:::preview{title="Week view with back-to-back, overlapping, all-day, and short events"}
<x-wirekit::event-calendar
    view="week"
    date="2026-06-08"
    aria-label="Weekly schedule"
    :day-markers="[
        ['date' => '2026-06-11', 'label' => 'Holiday', 'type' => 'holiday'],
    ]"
    :events="[
        ['id' => 1, 'title' => 'Standup', 'start' => '2026-06-08T09:00:00', 'end' => '2026-06-08T09:15:00', 'intent' => 'accent'],
        ['id' => 2, 'title' => '1:1 with Sam', 'start' => '2026-06-08T09:15:00', 'end' => '2026-06-08T10:00:00', 'intent' => 'success'],
        ['id' => 3, 'title' => 'Lunch', 'start' => '2026-06-09T12:00:00', 'end' => '2026-06-09T13:00:00'],
        ['id' => 4, 'title' => 'Demo', 'start' => '2026-06-10T15:00:00', 'end' => '2026-06-10T16:30:00', 'intent' => 'warning'],
        ['id' => 5, 'title' => 'Conference', 'start' => '2026-06-10T09:00:00', 'allDay' => true, 'intent' => 'accent'],
        ['id' => 6, 'title' => 'Sprint planning & backlog refinement', 'start' => '2026-06-12T09:00:00', 'end' => '2026-06-12T10:00:00', 'intent' => 'accent'],
        ['id' => 7, 'title' => 'Architecture review with the platform team', 'start' => '2026-06-12T10:00:00', 'end' => '2026-06-12T11:00:00', 'intent' => 'success'],
        ['id' => 8, 'title' => 'Customer onboarding — Acme Corp (kickoff)', 'start' => '2026-06-12T11:00:00', 'end' => '2026-06-12T12:30:00', 'intent' => 'warning'],
        ['id' => 9, 'title' => 'Design critique: checkout flow redesign', 'start' => '2026-06-12T13:00:00', 'end' => '2026-06-12T14:30:00', 'intent' => 'danger'],
    ]"
/>
:::

## Agenda View

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

:::preview{title="Agenda view with a day marker"}
<x-wirekit::event-calendar
    view="agenda"
    date="2026-06-08"
    aria-label="Upcoming events"
    :day-markers="[
        ['date' => '2026-06-11', 'label' => 'Public holiday', 'type' => 'holiday'],
    ]"
    :events="[
        ['id' => 1, 'title' => 'Standup', 'start' => '2026-06-08T09:00:00', 'end' => '2026-06-08T09:30:00', 'intent' => 'accent'],
        ['id' => 2, 'title' => 'Design review', 'start' => '2026-06-08T14:00:00', 'end' => '2026-06-08T15:00:00', 'intent' => 'success'],
        ['id' => 3, 'title' => 'Release 2.6', 'start' => '2026-06-10T16:00:00', 'end' => '2026-06-10T17:00:00', 'intent' => 'danger'],
        ['id' => 4, 'title' => 'Team offsite', 'start' => '2026-06-12T09:00:00', 'allDay' => true],
    ]"
/>
:::

## 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.

:::preview{title="Holidays and blocked days"}
<x-wirekit::event-calendar
    view="month"
    date="2026-12-20"
    aria-label="December schedule"
    :day-markers="[
        ['date' => '2026-12-24', 'label' => 'Christmas Eve', 'type' => 'working'],
        ['date' => '2026-12-25', 'label' => 'Christmas Day', 'type' => 'holiday', 'blocked' => true],
        ['date' => '2026-12-26', 'label' => 'Boxing Day', 'type' => 'holiday', 'blocked' => true],
        ['date' => '2027-01-01', 'label' => 'New Year', 'type' => 'holiday', 'blocked' => true],
    ]"
    :events="[
        ['id' => 1, 'title' => 'Year-end sync', 'start' => '2026-12-22T10:00:00', 'end' => '2026-12-22T11:00:00', 'intent' => 'accent'],
    ]"
/>
:::

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) |
| `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) |

## 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`.

:::preview{title="Sunday-start month grid"}
<x-wirekit::event-calendar
    view="month"
    date="2026-06-15"
    :week-starts-on="0"
    aria-label="Sunday-start schedule"
    :events="[
        ['id' => 1, 'title' => 'Brunch', 'start' => '2026-06-14T11:00:00', 'end' => '2026-06-14T12:00:00', 'intent' => 'success'],
        ['id' => 2, 'title' => 'Sprint planning', 'start' => '2026-06-15T10:00:00', 'end' => '2026-06-15T11:00:00', 'intent' => 'accent'],
    ]"
/>
:::

## Livewire Integration

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

```blade
{{-- 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)"
/>
```

```php
// 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);
}
```

## Event Shape

| Key | Required | Description |
| --- | --- | --- |
| `id` | yes | Unique identifier (emitted on click) |
| `title` | yes | Event title |
| `start` | yes | ISO 8601 start datetime |
| `end` | no | ISO 8601 end (defaults to start + 1 hour) |
| `allDay` | no | `true` renders "All day" in agenda |
| `intent` | no | `accent` (default) · `success` · `warning` · `danger` · `neutral` |

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `events` | array | `[]` | Events (see shape above) |
| `dayMarkers` | array | `[]` | Day-level holiday / working / note markers (see [shape](#day-marker-shape)) |
| `view` | string | `'month'` | `month` · `week` · `agenda` |
| `date` | string\|null | `null` | ISO date the calendar opens on (default today) |
| `weekStartsOn` | int | `1` | First column: `0` (Sunday) – `6` (Saturday); `1` (Monday) is the default |
| `ariaLabel` | string | `'Calendar'` | Accessible name for the calendar |
| `scope` | string\|null | `null` | Scoped personalization name |

## 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.
- The week time grid is a labeled `role="region"` with `tabindex="0"` and a
  focus ring, so the scrolling grid is keyboard-reachable (WCAG 2.1.1).
- The title is an `aria-live="polite"` region, so navigating months / weeks is
  announced.
- 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).

## Keyboard Interaction

| Key | Action |
| --- | --- |
| `Tab` | Move through navigation, the view switcher, and event blocks |
| `Enter` / `Space` | Activate an event, navigate, or switch view |

## 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`:

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

## Usage & Conventions

::: info
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

- [WAI-ARIA Authoring Practices — Tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/)
- [MDN: `Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)
