---
## Calendar
Flexible calendar component with month, year, week, and day views.
Renders timed appointments, multi-day spans and recurrence on a time grid, with
event display, date selection and configurable layout. For a headless grid that
buckets your own domain content (meals, shifts, bookings) per day, use `Planner`.
For one lane per resource (rooms, chairs, vehicles) with bars spanning the days
each is occupied, use `ResourceTimeline`.
**Import:** `import { Calendar } from '@urbicon-ui/blocks';`
### Examples
```svelte
```
```svelte
```
```svelte
{#snippet children()}
{/snippet}
```
```svelte
```
```svelte
holidays.has(toIso(d)) || d.getDay() === 0 || d.getDay() === 6}
/>
```
### Variants
- variant: bordered, default, ghost (default: default)
- size: lg, md, sm (default: md)
- navPlacement: next
- showToday: false, true
- showWeekNumbers: false, true
- stacksOnNarrow: false, true
### Api
| Prop | Type | Required | Default | Description |
| --- | --- | :---: | --- | --- |
| ...CalendarVariants | `VariantProps` | no | | Styling variants from CalendarVariants |
| ...HTMLAttributes | `HTMLAttributes` | no | | HTML attributes (excluding: 'children') |
| agendaDays | `number` | no | 30 | How many days the agenda lists, counted **from the reference date** — the day `value`/`defaultDate` anchors on, or today. So `agendaDays={1}` next to a `defaultDate` is one day's list, and the default is roughly a month ahead of that day rather than the calendar month it sits in. The arrows, `ArrowLeft`/`ArrowRight` and the swipe step the whole window, so the next list starts the day after the current one ends, and `minDate`/ `maxDate` bound the WINDOW rather than its anchor: a step clamps span-preserving and the arrows disable once an edge is reached. Navigation reports through `onNavigate` — uniformly, including at `agendaDays={1}`, rather than switching callbacks on a prop value; the header's month picker still reports `onMonthChange`, because a month is what it picks. The window is always exactly this many days; the RENDERING skips days with no events, so a month-long window of one busy day is one heading, and a window of nothing renders the empty state. Values outside 1–366 (and non-numbers) fall back to the default and warn in DEV — a list is not a place to accidentally walk a decade. |
| animated | `boolean` | no | true | Enable animated transitions for navigation. |
| categories | `DateCategory[]` | no | [] | Event categories for color coding and legend. |
| children | `Snippet` | no | | Default children snippet for custom layout composition. |
| class | `string` | no | | Extra CSS classes on the root element. In the same Tailwind bucket `class` wins over the `slotClasses` entry for the element it lands on. Presets and provider defaults sit below both. |
| dayCell | `Snippet<[DayCellContext]>` | no | | Renders the content of each day in the month grid, outside-month days included. Calendar wraps it in the day's `role="gridcell"` and sets `aria-selected`, `aria-disabled` and `aria-current="date"` there, so the snippet renders content, not a cell — no `role` of its own, and no layout that expects to be the week row's grid item. The wrapper takes no clicks and no focus: a day that should respond to the pointer or the keyboard renders its own control inside it. For the arrow keys to land on it, that control must be focusable and carry `data-date` as `YYYY-MM-DD` — the grid focuses the `[data-date]` of the day the keys reach, and moves on from the one that has focus — and `tabindex={isFocused ? 0 : -1}` keeps it a single tab stop. Enter and Space on it select its date through Calendar; they do not reach the control's own click handler. A control without `data-date` keeps its own keys. |
| defaultDate | `Date` | no | | Initial reference day the grid is anchored on, without selecting it. Use this to open a **week** or **day** view on a specific week — `defaultMonth`/ `defaultYear` resolve to the 1st, whose week can fall mostly in the previous month. Ignored when `value` is set (the selection anchors instead); takes priority over `defaultMonth`/`defaultYear`. Read at mount only. |
| defaultMonth | `number` | no | | Initial displayed month (0–11). Used only when `value` and `defaultDate` are unset; when `value` is provided, the calendar opens on the value's month. Best for month/year views — for week/day views prefer `defaultDate`. Defaults to current month. |
| defaultYear | `number` | no | | Initial displayed year. Used only when `value` is unset; when `value` is provided, the calendar opens on the value's year. Defaults to current year. |
| disabled | `boolean` | no | false | Disable the entire calendar. |
| draggable | `boolean` | no | false | Enable drag & drop to move events between dates. |
| eventItem | `Snippet<[EventItemContext]>` | no | | Custom snippet for rendering an event item in the list-based views (agenda and the month event list). Time-grid views (week/day) render events through their hour grid and ignore this snippet. Replaces the default row entirely, including the clock time an event with `allDay: false` shows there — format it from `event.start`/`event.end` yourself (`formatTimeRange` from `@urbicon-ui/blocks/date` is the same helper the default uses). For a multi-day event the context's `isStart` / `isEnd` say which end of the span this row is: the default prints `start` on the first day, "until " on the last, nothing in between, because `event.start` is the same instant on every row. The chronological order of a day is not part of the snippet's job: the calendar sorts each day before rendering. |
| eventPopover | `boolean` | no | false | Show a rich popover on hover/focus for days with events (month view). |
| events | `CalendarEvent[]` | no | [] | Array of events to display on the calendar. |
| fixedWeeks | `boolean` | no | false | Always show 6 weeks in the grid. |
| header | `Snippet<[HeaderContext]>` | no | | Custom snippet for the header area. |
| highlightToday | `boolean` | no | true | Visually mark today across every view — the month cell, the week column header, the year mini-day, the agenda day header and the mini calendar. `aria-current="date"` is **not** affected: it is a semantic pointer, so a screen-reader user keeps the orientation a purely visual preference should not take away. Neither is the time grid's current-time line, which marks the current *time* rather than the day (see `timeGridHourHeight`'s neighbours). Matches `Planner`'s prop of the same name. |
| isDateDisabled | `(date: Date) => boolean` | no | | A day it returns `true` for cannot be selected or created by double-click, on top of `minDate`/`maxDate` — the one way to close single days, whether a rule (weekends) or a list (holidays). The month grid marks it, and its arrow keys still land on it; the week view's day headings and the mini calendar do not mark it, but selecting it there does nothing. Dropping an event on it, creating one in the time grid and the mini calendar's navigation do not ask the predicate. It receives a `Date` of its own for each day — read only its calendar day. It runs again when `$state` it reads changes. Compare calendar days, not `Date` objects (`list.includes(d)` matches nothing): keep a list as a `Set` of `toIso` keys from `@urbicon-ui/blocks/date` and pass `(d) => blocked.has(toIso(d))`. It must not throw; the calendar does not catch it. |
| locale | `string` | no | 'auto' | BCP 47 locale tag for date formatting — month names, weekday names, the header title and the clock time of timed events in the list-based views (hour cycle, separator and padding all follow the locale, so a 12-hour locale renders "9:05 AM"). Defaults to `'auto'`, which follows the active `` locale, so an app that already declares its language does not have to repeat it here. SSR-safe: the locale comes from context, so the server and the client resolve the same tag (`Intl` with `undefined` would follow the runtime and disagree across hydration). Falls back to the base locale (`en`) when no provider is mounted. Pass an explicit tag (e.g. `'de-DE'`, `'ja-JP'`) to override. |
| maxDate | `Date` | no | | Latest selectable/navigable date. |
| minDate | `Date` | no | | Earliest selectable/navigable date. |
| miniCalendarPosition | `'left' | 'right'` | no | 'left' | Position of the mini calendar sidebar. |
| onDateClick | `(date: Date) => void` | no | | Fires when a date cell is clicked (regardless of selection change). |
| onDateCreate | `(date: Date, view: CalendarViewMode) => void` | no | | Fires on double-click on a day cell for event creation. The consumer shows their own form. |
| onDayChange | `(date: Date) => void` | no | | Fires when the displayed day changes (day view). |
| onEventClick | `(event: CalendarEvent) => void` | no | | Fires when an event is clicked. |
| onEventMove | `(event: CalendarEvent, newStart: Date, newEnd: Date) => void` | no | | Fires when an event is moved via drag & drop. |
| onEventResize | `(event: CalendarEvent, newEnd: Date) => void` | no | | Fires when an event is resized via drag handle. |
| onMonthChange | `(month: number, year: number) => void` | no | | Fires when the displayed month/year changes via navigation — the month and year views, and the header's month picker in any view. The agenda's arrows step a window of days instead of a month and report through `onNavigate`. |
| onNavigate | `(date: Date, range: DateRange) => void` | no | | Fires after **any** navigation, in every view, with the new reference date and the visible range — load data here. A **view switch** counts as one: it changes the window (a padded month grid, seven days, `agendaDays` from the anchor …) without moving the reference date, and a loader that missed it rendered the new view against the old view's rows. The per-view callbacks (`onMonthChange` / `onWeekChange` / `onDayChange`) still fire and are the better fit when you only care about one view; this one spares you reconstructing the window yourself — and in the agenda it is the *only* navigation callback, whose window no per-view callback can name. The range is view-accurate: month spans the padded cell grid (spill days included), week/day the visible days, year 1 Jan–31 Dec, agenda `agendaDays` from the reference date. Matches `Planner`'s `onNavigate`. |
| onTimeSlotCreate | `(start: Date, end: Date) => void` | no | | Fires on click on an empty time slot for event creation. Returns default 1h duration. |
| onValueChange | `(value: CalendarSelection) => void` | no | | Fires when the selected date(s) change. |
| onViewChange | `(view: CalendarViewMode) => void` | no | | Fires when the view mode changes — before `onNavigate`, which the switch also fires because the visible window changed with it. A switch to the view already on screen is a no-op and fires neither. |
| onWeekChange | `(weekStart: Date) => void` | no | | Fires when the displayed week changes (week view). |
| preset | `string` | no | | Apply a named preset registered via ``. Prefer this over `class` overrides when the requested look falls outside the semantic intent palette — presets keep hover/active/dark-mode logic coherent and make the custom look reusable across the project. |
| resizable | `boolean` | no | false | Enable resize handles on timed events in the time grid. |
| selectionMode | `'single' | 'range' | 'multiple'` | no | 'single' | Selection behavior. |
| showEventList | `boolean` | no | | Whether to show the detail list when a date is selected. Auto-enabled when events are provided. |
| showLegend | `boolean` | no | | Whether to show the built-in legend. Defaults to true when categories are provided. |
| showMiniCalendar | `boolean` | no | false | Show a mini month calendar sidebar (week/day/agenda views). |
| showOutsideDays | `boolean` | no | true | Show days from previous/next months to fill the grid. |
| showViewSwitcher | `boolean` | no | true | Show the view switcher in the header. Below `sm` its labels condense to their short form; the full label stays the accessible name. |
| showWeekNumbers | `boolean` | no | false | Show ISO week numbers in the left margin. |
| size | `'sm' | 'md' | 'lg'` | no | 'md' | Component size. |
| slotClasses | `Partial>` | no | | Per-slot class overrides. In the same Tailwind bucket `class` wins over the `slotClasses` entry for the element it lands on. Presets and provider defaults sit below both. |
| swipeable | `boolean` | no | true | Enable swipe gestures for touch navigation. |
| timeGridEndHour | `number` | no | 20 | Last visible hour in time grid (exclusive). |
| timeGridHourHeight | `number` | no | | Height of one hour row in the time grid, in pixels. Left unset it follows `size` (sm 40 · md 48 · lg 64), which is the only reason a nine-hour day costs 432 px of card height whether or not the consumer has it. Set a smaller number for a compact day, a larger one for finer slots. Drives the label column, the slot rows, the grid's `min-height` and the auto-scroll to the current time, so it is a number rather than a CSS variable — the scroll math has to read it. |
| timeGridInterval | `30 | 60` | no | 60 | Time slot interval in minutes. |
| timeGridStartHour | `number` | no | 7 | First visible hour in time grid. |
| unstyled | `boolean` | no | | Strip all default tv() classes. |
| value | `CalendarSelection` | no | | Currently selected date(s). Supports bind:value. |
| variant | `'default' | 'bordered' | 'ghost'` | no | 'default' | Visual style. |
| view | `CalendarViewMode` | no | 'month' | Active view mode. Supports bind:view. `week` and `day` are hour grids. The week's seven columns keep at least `--blocks-calendar-day-min-width` each (5 rem at `size="sm"`, 6 rem at `md`, 7 rem at `lg`); once they no longer fit, the grid scrolls sideways instead of shrinking the days to a stripe. The hour gutter stays pinned to the left while it does and the day heads — with the all-day band under them — to the top, and arrow-key navigation brings the focused day into view. Set the property on the calendar — `style="--blocks-calendar-day-min-width: 8rem"` — to trade more scrolling for wider days. While the week is scrolling, a horizontal touch drag belongs to the scroller: it moves the days rather than paging to the next week. A week that fits keeps the swipe. The header arrows, `ArrowLeft`/`ArrowRight` and `bind:view` are unaffected either way. On a phone the honest week is often no week at all: bind this prop to a `MediaQuery` and narrow `views` with it (the `MediaQuery` example on the component) rather than handing over seven columns behind a scrollbar. |
| views | `CalendarViewMode[]` | no | ['month', 'week', 'day', 'year', 'agenda'] | Which views the header's switcher offers. It filters the switcher only: a `view` left out of the list still renders, so narrowing the list is how a layout hides a view it cannot serve while still choosing it itself — the viewport-driven pairing under `view`. |
| weekStartsOn | `0 | 1 | 2 | 3 | 4 | 5 | 6` | no | 1 | First day of the week. 0 = Sunday, 1 = Monday. |
| navPlacement | `'next'` | no | | Controls the navPlacement behavior and appearance of the Calendar component. Available options: next. |
| showToday | `'false' | 'true'` | no | | Controls the showToday behavior and appearance of the Calendar component. Available options: false, true. |
| stacksOnNarrow | `'false' | 'true'` | no | | Controls the stacksOnNarrow behavior and appearance of the Calendar component. Available options: false, true. |
Inherited from:
- Omit (omit-pattern)
- Omit, 'children'> (omit-pattern)
### Types
```ts
type CalendarSlotName = CalendarSlots
```
```ts
interface CalendarEvent {
/** Unique identifier. */
id: string;
/** Event title. */
title: string;
/** Start date/time. */
start: Date;
/** End date/time. Optional for all-day events. */
end?: Date;
/**
* Whether this is an all-day event. An explicit `false` is what marks an
* event as happening AT a time: it then lands on the week/day time grid, and
* the list-based views (agenda, month event list) print its clock time ahead
* of the title and sort it after the day's all-day events.
*
* A timed event spanning several days states one time per row, the one that
* is true for that day: `start` (with `end`, when `end` falls on the same
* calendar day) on its first day, "until " on its last, and nothing on
* the days in between — those rows carry the "Day 2 of 3" badge instead.
* @default true
*/
allDay?: boolean;
/** Category ID linking to a {@link DateCategory} in `categories`. */
categoryId?: string;
/** Optional description or status text. */
description?: string;
/** Optional secondary helper text. */
helperText?: string;
/** Arbitrary metadata for consumer use. */
meta?: Record;
/** Recurrence rule for repeating events. */
recurrence?: RecurrenceRule;
}
```
```ts
type CalendarSelection = Date | DateRange | Date[]
```
```ts
type CalendarViewMode = 'month' | 'year' | 'week' | 'day' | 'agenda'
```
```ts
interface DateCategory {
/** Stable id — what an item's category accessor returns. */
id: string;
/** Display label, shown in the legend. */
label: string;
/**
* CSS colour value for the dot, bar or border. Where a label sits on it (a
* Calendar event bar, a ResourceTimeline span), the component picks black or
* white for contrast when the value is a hex, `rgb()` or `oklch()` colour —
* a translucent one judged over both a white and a black page, since either
* may show through. Anything else, a `var(--…)` or `color-mix()` included,
* gets a white label.
*/
color: string;
}
```
```ts
interface DateRange {
start: Date;
end: Date;
}
```
```ts
interface DayCellContext {
date: Date;
events: CalendarEvent[];
isToday: boolean;
isSelected: boolean;
isDisabled: boolean;
isOutsideMonth: boolean;
isFocused: boolean;
}
```
```ts
interface EventItemContext {
event: CalendarEvent;
category?: DateCategory;
/** 0-based index: which day of the multi-day event (only set for multi-day). */
dayIndex?: number;
/** Total duration in days (only set for multi-day, > 1). */
totalDays?: number;
/** Whether this date is the start of the event. */
isStart?: boolean;
/** Whether this date is the end of the event. */
isEnd?: boolean;
}
```
```ts
interface HeaderContext {
view: CalendarViewMode;
month: number;
year: number;
title: string;
navigate: (delta: number) => void;
navigateMonth: (delta: number) => void;
goToToday: () => void;
canGoBack: boolean;
canGoForward: boolean;
/** Whether today is within `[minDate, maxDate]`; gate a custom Today control on it. */
canGoToToday: boolean;
}
```
```ts
interface RecurrenceRule {
/** How often the event repeats. */
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
/** Repeat every N periods (e.g. 2 = every 2 weeks). @default 1 */
interval?: number;
/**
* Weekdays the rule applies to — 0=Sun, 1=Mon, ..., 6=Sat. Values outside
* that range match no day at all and warn in DEV. An empty array reads as
* "no restriction", not "exclude everything".
*
* Its meaning depends on `frequency`, following RFC 5545:
* - `weekly` — GENERATES one occurrence per listed day, in every interval-th
* week. `{ weekly, interval: 2, byDay: [1,2,3,4,5] }` = weekdays of every
* other week.
* - `daily` — FILTERS the days the rule would otherwise produce.
* `{ daily, interval: 2, byDay: [1,2,3,4,5] }` = every other day, but only
* when it falls on a weekday.
*
* The two are not interchangeable once `interval` is involved.
*
* Only the occurrence's START day is tested, so on a multi-day event a
* weekday-only rule still draws across the weekend. Read by `daily` and
* `weekly` only; `monthly` uses `byMonthDay`, `yearly` repeats the start date.
*/
byDay?: number[];
/** Specific day(s) of month, 1-31. Read by `monthly` only. */
byMonthDay?: number[];
/** End date — recurrence stops after this date (inclusive). */
until?: Date;
/**
* Maximum number of occurrences the series yields. Days removed by `byDay`
* are not occurrences and do not count against it — `{ daily, count: 3,
* byDay: [1..5] }` starting on a Sunday yields the following Mon/Tue/Wed. The
* original start date counts when it is itself an occurrence.
*/
count?: number;
/** Exception dates where the event does NOT occur. */
exceptions?: Date[];
}
```
### Slots (slotClasses keys)
`base`, `header`, `title`, `nav`, `titleGroup`, `switcher`, `actions`, `navButton`, `grid`, `weekdayHeader`, `weekday`, `weekRow`, `weekNumber`, `day`, `dayNumber`, `dotContainer`, `dot`, `list`, `dateHeader`, `empty`, `item`, `colorBar`, `eventTime`, `eventTitle`, `eventDescription`, `eventHelper`, `legend`, `legendItem`, `legendDot`, `legendLabel`, `yearGrid`, `yearMonth`, `yearMonthTitle`, `yearMiniDay`, `yearMiniDot`, `weekColumnHeader`, `weekColumnDayName`, `weekColumnDayNumber`, `weekAllDayEvent`, `multiDayBar`, `multiDayBarContainer`, `dayView`, `dayViewHeader`, `agendaView`, `agendaDayGroup`, `agendaDayHeader`, `agendaEventList`, `timeGrid`, `timeGutter`, `timeHeadCell`, `timeCorner`, `timeLabel`, `timeSlotRow`, `timeDayColumn`, `timeEvent`, `allDayArea`, `currentTimeLine`, `weekTimeLayout`, `eventPopover`, `eventPopoverItem`, `miniCalendar`, `miniCalendarHeader`, `miniCalendarTitle`, `miniCalendarNavButton`, `miniCalendarWeekday`, `miniCalendarDay`