Events
Event input, time zones, ordering, hiding and the event store.
An event is timed or all-day, as its kind says. Both carry a unique id and a data payload of your own type.
export const bookings: Calendar_EventInput<Booking>[] = [
{
id: 'standup',
kind: 'timed',
start: at('2026-03-11T09:00'),
end: at('2026-03-11T09:15'),
resourceIds: ['room-a'],
data: { title: 'Standup' },
},
{
id: 'offsite',
kind: 'allDay',
start: Temporal.PlainDate.from('2026-03-12'),
end: Temporal.PlainDate.from('2026-03-13'),
data: { title: 'Offsite' },
},
];| Field | Timed | All-day |
|---|---|---|
id | string, unique | string, unique |
kind | 'timed' | 'allDay' |
start | Temporal.ZonedDateTime | Temporal.PlainDate |
end | Temporal.ZonedDateTime | Temporal.PlainDate, inclusive; defaults to start |
resourceIds | string[], optional | string[], optional |
display | 'auto' | 'background', optional | 'auto' | 'background', optional |
data | TData | TData |
- Timed events may be in any zone. The calendar converts them to its
timeZone, and the model holds the converted copies. - A timed event ending exactly at midnight ends on the previous day, so 22:00 to 00:00 stays on one day.
- Duplicate ids log an error in development. Ids also key the model's segments.
- Pass a new array to change events; the calendar compares the array by reference.
display: 'background'shades the event's time instead of laying it out. See background events.
createEvent returns its argument. Use it to type an event where you write it, with kind kept as a literal and the
payload type inferred from data:
// typed where it is written; `data` infers the payload type
export const review = createEvent({
id: 'review',
kind: 'timed',
start: Temporal.ZonedDateTime.from(`2026-03-11T14:00[${timeZone}]`),
end: Temporal.ZonedDateTime.from(`2026-03-11T15:00[${timeZone}]`),
data: { title: 'Review' },
});For repeating events, add the optional recurrence feature. One-off and recurring definitions share
the same events array and event store.
Hiding events
There is no hidden flag: filter the array. Memoize the result, so the calendar only re-derives when it changes.
// hiding is filtering the input; a memo keeps the array stable until its inputs change
export const useVisibleEvents = (events: Accessor<Calendar_EventInput<Booking>[]>, hidden: Accessor<Set<string>>) =>
createMemo(() => events().filter(event => !hidden().has(event.id)));Order
The calendar sorts the events in the visible range before layout, and the order sets bar rows and lanes. The default order is:
- Earlier start date first.
- Longer date spans first.
- All-day before timed.
- Earlier start time first.
Pass eventOrder(a, b, context) to replace it. context carries the view key and kind, so the order can differ per
view. It is read when the layout derives: changing it alone does not re-order until a tracked option changes.
// shortest first on the time grid, alphabetical everywhere else
export const eventOrder = (a: Calendar_Event<Booking>, b: Calendar_Event<Booking>, context: { kind: string }) =>
context.kind === 'timeGrid' && a.kind === 'timed' && b.kind === 'timed'
? a.start.until(a.end).total('minutes') - b.start.until(b.end).total('minutes')
: a.data.title.localeCompare(b.data.title);Event store
If you have no data cache of your own, the event store keeps the list for you. add(events) upserts by id, update(id, event => next) replaces one event, remove(id) deletes one and
reset(events) replaces the list. Every change produces a new array. Don't use it to mirror another store; pass that
store's data directly.
useCalendarEventStore returns add, update, remove and reset, plus events, an accessor for the current
list. Read it inside the useCalendar accessor, so the calendar follows it:
export const useBookingCalendar = () => {
const [view, setView] = createSignal<ViewKey>('week');
const [date, setDate] = createSignal(Temporal.Now.plainDateISO());
const eventStore = useCalendarEventStore<Booking>(bookings);
const calendar = useCalendar(() => ({
timeZone,
views,
features,
view: view(),
date: date(),
events: eventStore.events(), // an accessor: read it inside
onNavigate: next => {
setView(next.view);
setDate(next.date);
},
}));
const book = (booking: Calendar_EventInput<Booking>) => eventStore.add([booking]);
const cancel = (id: string) => eventStore.remove(id);
return { calendar, book, cancel };
};Clicking events
Event parts are native buttons with no behavior. Pass onClick to open a drawer, a popover or a details page, or
render them as links.