Moltenship Time
Concepts

State

Let the calendar own its state, or control it from your app, one value at a time.

Root keeps six values:

ValueTypeMeaning
viewone of your view namesThe active view
anchorDateTemporal.PlainDateThe date the visible range is built around
timeZonestring (IANA name)The zone timed events are shown in
selectionyour type, or nullWhat the user selected
focusyour type, or nullWhat has keyboard focus inside the calendar
expandedResourceIdsarray of resource IDsWhich resource groups are expanded

Each value can be uncontrolled (the calendar owns it) or controlled (your app owns it), independently of the others.

Uncontrolled

Pass a default prop and the calendar manages the value from then on. Defaults are read once, when Root mounts.

<Calendar.Root
  events={meetings}
  defaultView="day"
  defaultAnchorDate={Temporal.PlainDate.from("2026-10-08")}
  defaultTimeZone="Europe/Berlin"
  locale="en-US"
  weekRules={weekRules}
>
  <Calendar.ActiveView />
</Calendar.Root>

Controlled

Pass the value and a change handler. The calendar never changes a controlled value itself. When the user clicks Next, it calls your handler with the new value, and you decide whether to store it.

This example controls the date so a date input outside the calendar stays in sync. It uses the Calendar from the Quick start.

controlled-state.tsx
export function ControlledCalendar() {
  const [anchorDate, setAnchorDate] = useState(() => Temporal.PlainDate.from("2026-10-08"));

  return (
    <>
      <input
        type="date"
        value={anchorDate.toString()}
        onChange={(event) => {
          if (event.currentTarget.value) {
            setAnchorDate(Temporal.PlainDate.from(event.currentTarget.value));
          }
        }}
      />
      <Calendar.Root
        events={meetings}
        anchorDate={anchorDate}
        onAnchorDateChange={setAnchorDate}
        defaultTimeZone="Europe/Berlin"
        locale="en-US"
        weekRules={weekRules}
      >
        <Calendar.PreviousTrigger>Previous</Calendar.PreviousTrigger>
        <Calendar.NextTrigger>Next</Calendar.NextTrigger>
        <NextWeekButton />
        <Calendar.ActiveView />
      </Calendar.Root>
    </>
  );
}

The props follow one pattern:

UncontrolledControlledHandler
defaultViewviewonViewChange
defaultAnchorDateanchorDateonAnchorDateChange
defaultTimeZonetimeZoneonTimeZoneChange
defaultSelectionselectiononSelectionChange
defaultFocusfocusonFocusChange
defaultExpandedResourceIdsexpandedResourceIdsonExpandedResourceIdsChange

A few rules:

  • undefined means uncontrolled. To control selection or focus with nothing selected, pass null.
  • Handlers also run for uncontrolled values, after the calendar updates them. The second argument tells you the previous value and whether the value is controlled.
  • If you haven't applied a change the calendar asked for, the handler also hears a later request for the value you have. For example, Today after a scroll you deferred. Apply the latest value you were given, and you'll drop the earlier change.
  • Root needs a date, a time zone, and a view on its first render. The view can come from defaultView, view, or the first registered view. If one is missing, Root throws calendar-root-requires-anchor-date, calendar-root-requires-time-zone, or calendar-root-requires-view.

Reading and changing state

Any component inside Root can use the hooks on your calendar:

  • useCalendarState() returns all six values.
  • useCalendarSelector(selector) returns one derived value and re-renders only when it changes.
  • useCalendarActions() returns setView, setAnchorDate, setTimeZone, setSelection, setFocus, setExpandedResourceIds, navigateNext, navigatePrevious, and navigateToday.
  • useCalendarMeta() returns everything else: the event snapshot, resources, the active view, diagnostics, and element IDs and refs.
  • useCalendar() returns state, actions, and meta together.
  • useVisibleRange() and useViewProjection() return what the active view shows. See Custom views.
controlled-state.tsx
const selectAnchorDate = (state: CalendarPublicState) => state.anchorDate;

function NextWeekButton() {
  const anchorDate = Calendar.useCalendarSelector(selectAnchorDate);
  const actions = Calendar.useCalendarActions();

  return (
    <button type="button" onClick={() => actions.setAnchorDate(anchorDate.add({ weeks: 1 }))}>
      Skip a week
    </button>
  );
}

A selector can live outside the component, as above, or inline. Actions called one after another build on each other, so navigateNext() twice in one click moves two steps, controlled or not.

Selection and focus types

You choose the shape of selection and focus. Pass their types as the third and fourth type arguments of createCalendar, after the event and resource types. Time stores the values as they are.

selection.tsx
type Selection = { readonly eventId: string } | { readonly date: Temporal.PlainDate };

const Calendar = createCalendar<Meeting, never, Selection>()({ eventAdapter });

export function SelectStandup() {
  const actions = Calendar.useCalendarActions();

  return (
    <button type="button" onClick={() => actions.setSelection({ eventId: "standup" })}>
      Select standup
    </button>
  );
}
Edit on GitHub

Last updated on

On this page