---
## Planner
**Stability:** beta — the API shape is settled and documented; behaviour-level fixes may still land as breaking changes in a minor.
Date-indexed planning board — a week, month or custom-range grid
whose cells hold YOUR domain content (meals, shifts, bookings, content slots)
via a generic `cell` snippet. Buckets `items` by calendar day, then handles
navigation, ISO week numbers, keyboard a11y and a responsive column→stack
layout. For timed appointments, multi-day spans or recurrence use `Calendar`
instead.
**Import:** `import { Planner } from '@urbicon-ui/blocks';`
### Examples
```svelte
e.date}
sort={(a, b) => MEAL_ORDER[a.mealType] - MEAL_ORDER[b.mealType]}
bind:value={referenceDate} onNavigate={(_, range) => loadWeek(range.start)}>
{#snippet cell({ items, isoDate })}
{#each items as entry (entry.id)}
{/each}
{/snippet}
```
```svelte
e.date}
isDateDisabled={(d) => closed.has(toIso(d))}
/>
```
### Variants
- variant: bordered, default, ghost (default: default)
- size: lg, md, sm (default: md)
### Api
| Prop | Type | Required | Default | Description |
| --- | --- | :---: | --- | --- |
| getDate | `(item: T) => Date | string` | yes | | Map an item to its calendar day. Return a `Date`, or a local date string (`'2026-06-16'`) taken verbatim — never UTC-parsed, so a plain date never shifts across timezones. A date-*time* string is bucketed by its written date part too; if your value is a UTC instant whose local day matters (`'…T23:00:00Z'`), return `new Date(value)` so the local timezone applies. Required. |
| ...HTMLAttributes | `HTMLAttributes` | no | | HTML attributes (excluding: 'children') |
| ...PlannerVariants | `VariantProps` | no | | Styling variants from PlannerVariants |
| animated | `boolean` | no | true | Slide-transition the grid on navigate (respects reduced-motion). |
| cell | `Snippet<[PlannerCellContext]>` | no | | Render a day's content — the core of the API. Receives bucketed `items: T[]`. Called for **every** day, including empty ones (`items: []`) — unless an `empty` snippet is given, which then handles empty days instead. Put an "add" affordance here to keep it available on empty days. |
| class | `string` | no | | Extra classes merged onto 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. |
| dayHeader | `Snippet<[PlannerDayContext]>` | no | | Customise each weekday/column header. |
| disabled | `boolean` | no | false | Disable navigation and selection. |
| empty | `Snippet<[PlannerCellContext]>` | no | | Placeholder rendered **instead of** `cell` for days with no items. Omit it to let `cell` render empty days too. |
| fixedWeeks | `boolean` | no | false | Always render 6 week rows in `view="month"`, so the grid keeps its height across months of 4, 5 and 6 rows. Ignored in `week`/`range`. |
| header | `Snippet<[PlannerHeaderContext]>` | no | | Replace the default toolbar (prev/next/today/title/week). |
| highlightToday | `boolean` | no | true | Visually mark today's cell. |
| highlightWeekend | `boolean` | no | false | Tint Saturday/Sunday cells. |
| isDateDisabled | `(date: Date) => boolean` | no | | Disables every day it returns `true` for, on top of `minDate`/`maxDate` — the one way to disable single days, whether a rule (weekends) or a list (holidays). A disabled day is dimmed, reports `aria-disabled` and cannot be selected. 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 grid does not catch it. |
| items | `T[]` | no | | The items to lay out. Each is bucketed onto a day via `getDate`. |
| locale | `string` | no | 'auto' | BCP 47 locale tag for date formatting — month names, weekday names and the header title. 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 navigable/selectable date. |
| minDate | `Date` | no | | Earliest navigable/selectable date. |
| onDateSelect | `(date: Date) => void` | no | | Fires when a day cell is activated (click / Enter / Space). |
| onNavigate | `(date: Date, range: DateRange) => void` | no | | Fires after navigation. Receives the new reference date and visible range — load data here. |
| preset | `string` | no | | Apply a named preset registered via ``. |
| rangeEnd | `Date` | no | | End of the window for `view="range"`. |
| rangeStart | `Date` | no | | Start of the window for `view="range"`. |
| selectedDate | `Date` | no | | The active highlighted day. Supports `bind:selectedDate`. |
| showWeekNumber | `boolean` | no | | Renamed to `showWeekNumbers` for parity with `Calendar`, which has carried the plural since long before Planner existed. Still honoured, and warns in DEV. |
| showWeekNumbers | `boolean` | no | false | Show the ISO week-number column on the left. |
| size | `'sm' | 'md' | 'lg'` | no | 'md' | Density of the grid and header. |
| slotClasses | `Partial>` | no | | Per-slot class overrides merged with tv() styles. Slots: base | header | headerTitle | nav | navButton | grid | weekdayHeader | weekday | weekNumber | week | cell | cellHeader | cellWeekday | cellDate | cellItems | empty. In the same Tailwind bucket `class` wins over the `slotClasses` entry for the element it lands on. Presets and provider defaults sit below both. |
| sort | `(a: T, b: T) => number` | no | | Comparator for items within a day cell (e.g. by meal type or start label). |
| swipeable | `boolean` | no | true | Enable horizontal swipe-to-navigate on touch. |
| unstyled | `boolean` | no | false | Remove all default tv() classes — only user-provided classes apply. Reaches the header's today-button tooltip. |
| value | `Date` | no | today | Reference date the view is anchored on. Supports `bind:value`. |
| variant | `'default' | 'bordered' | 'ghost'` | no | 'default' | Visual treatment. |
| view | `PlannerView` | no | 'week' | Layout mode. |
| weekStartsOn | `0 | 1 | 2 | 3 | 4 | 5 | 6` | no | 1 | First day of the week (0=Sun … 6=Sat). |
Inherited from:
- Omit (omit-pattern)
- Omit, 'children'> (omit-pattern)
### Types
```ts
type PlannerCellState = Pick<
PlannerVariants,
'dayState' | 'selected' | 'weekend' | 'outside'
>
```
```ts
interface DateRange {
start: Date;
end: Date;
}
```
```ts
interface PlannerCellContext {
/** The cell's date (local midnight). */
date: Date;
/** `YYYY-MM-DD` local key for this date — the same key items were bucketed on. */
isoDate: string;
/** Items falling on this day, bucketed and sorted (the consumer's `T`). */
items: T[];
/** Whether this date is today. */
isToday: boolean;
/** Whether this date is the active selected day (`selectedDate`). */
isSelected: boolean;
/** Whether this date is a Saturday or Sunday. */
isWeekend: boolean;
/** Whether this date spills outside the focused month / requested range. */
isOutsideRange: boolean;
/**
* Whether this date cannot be selected — outside `[minDate, maxDate]`, rejected by
* `isDateDisabled`, or the whole grid is `disabled`. The scaffold already sets
* `aria-disabled` on the gridcell; read this to grey out your own content or hide an
* "add" affordance.
*/
isDisabled: boolean;
/** ISO 8601 week number of this date. */
weekNumber: number;
/** Localized short weekday name (e.g. "Mo") — handy for stacked layouts. */
weekday: string;
/** Make this day the active selection (fires `onDateSelect`). */
selectDate: () => void;
}
```
```ts
interface PlannerDayContext {
/** The concrete date in single-week (`week`) view; `undefined` in month/range
* where a column spans many dates. */
date?: Date;
/** `YYYY-MM-DD` local key, present only when `date` is. */
isoDate?: string;
/** Localized short weekday name (e.g. "Mo"). */
weekday: string;
/** Whether the column's concrete date is today (always false in month/range). */
isToday: boolean;
/** Whether this is a weekend column. */
isWeekend: boolean;
/** ISO week number of the column's date, when it has one. */
weekNumber?: number;
}
```
```ts
interface PlannerHeaderContext {
/** Localized title for the current view (e.g. "KW 25 · 15.–21. Juni"). */
title: string;
/** First visible date of the view. */
rangeStart: Date;
/** Last visible date of the view. */
rangeEnd: Date;
/** ISO week number of the reference date. */
weekNumber: number;
/** The active view. */
view: PlannerView;
/** Step the view by `delta` periods (− back, + forward). */
navigate: (delta: number) => void;
/** Jump to the period containing today. */
goToToday: () => void;
/** Jump to the period containing `date`. */
goTo: (date: Date) => void;
/** Whether back-navigation is within bounds. */
canGoBack: boolean;
/** Whether forward-navigation is within bounds. */
canGoForward: boolean;
/** Whether today is within `[minDate, maxDate]`; gate a custom Today control on it. */
canGoToToday: boolean;
}
```
```ts
type PlannerView = 'week' | 'month' | 'range'
```
### Slots (slotClasses keys)
`base`, `grid`, `weekdayHeader`, `weekday`, `weekNumber`, `week`, `cell`, `cellHeader`, `cellWeekday`, `cellDate`, `cellItems`, `empty`