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.

// hiding is filtering the input; memoize so the array only changes when its inputs do
export const useVisibleEvents = (events: Calendar_EventInput<Booking>[], hidden: Set<string>) =>
  useMemo(() => events.filter(event => !hidden.has(event.id)), [events, hidden]);

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.

useCalendarEventStore creates one per component. It returns add, update, remove and reset, plus events, the current list, which re-renders the component when it changes:

export const useBookingCalendar = () => {
  const [view, setView] = useState<ViewKey>('week');
  const [date, setDate] = useState(() => Temporal.Now.plainDateISO());
  const eventStore = useCalendarEventStore<Booking>(bookings);

  const calendar = useCalendar({
    timeZone,
    views,
    features,
    view,
    date,
    events: eventStore.events,
    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.