calendar-tsx

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' },
  },
];
FieldTimedAll-day
idstring, uniquestring, unique
kind'timed''allDay'
startTemporal.ZonedDateTimeTemporal.PlainDate
endTemporal.ZonedDateTimeTemporal.PlainDate, inclusive; defaults to start
resourceIdsstring[], optionalstring[], optional
display'auto' | 'background', optional'auto' | 'background', optional
dataTDataTData
  • 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.

Order

The calendar sorts the events in the visible range before layout, and the order sets bar rows and lanes. The default order is:

  1. Earlier start date first.
  2. Longer date spans first.
  3. All-day before timed.
  4. 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.

createEventStore returns a TanStack Store holding { events }, and the add, update, remove and reset actions. Pass its events to the calendar, and again whenever they change:

const eventStore = createEventStore<Booking>(bookings);

const calendar = createCalendar({
  timeZone,
  views,
  features,
  view: 'week',
  date: Temporal.Now.plainDateISO(),
  events: eventStore.store.state.events,
  onNavigate: next => calendar['~update']({ ...calendar.options, view: next.view, date: next.date }),
});

// pass every new list to the calendar as ordinary input
eventStore.store.subscribe(({ events }) => calendar['~update']({ ...calendar.options, events }));

eventStore.add([
  {
    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' },
  },
]); // upserts by id
eventStore.update('review', event => ({ ...event, data: { title: 'Design review' } }));
eventStore.remove('standup');