calendar-tsx

Navigation

Move between views and dates, build a toolbar and sync the URL.

view and date are controlled. The navigation actions never change the calendar: each computes the next view and date and calls onNavigate with them, and the calendar moves when you pass them back in.

Actions

ActionNext view and date
next(), prev()One period forward or back: the configured calendar period, months, weeks, or days.
today()Today in timeZone, from the clock.
goTo(date)Same view, date.
setView(view, date?)view, keeping the current date unless one is given.
navigate({ view, date })Either or both; omitted fields keep their current value.

onNavigate receives { view, date, range, period, inputRange }. The next range arrives with the call, so you can update the URL or start fetching events before the calendar re-renders.

Toolbar

Call the actions on the instance from useCalendar, and select the context for the title and the current view:

toolbar.tsx
export const Toolbar = ({ calendar }: { calendar: AppCalendar }) => {
  const context = useCalendarSelector((state: State) => state.context);
  const title = context.period.start.toLocaleString('en-US', { month: 'long', year: 'numeric' });

  return (
    <header>
      <button onClick={() => calendar.api.prev()}>Previous</button>
      <button onClick={() => calendar.api.today()}>Today</button>
      <button onClick={() => calendar.api.next()}>Next</button>
      <h2>{title}</h2>
      <select value={context.view} onChange={event => calendar.api.setView(event.target.value as ViewKey)}>
        <option value='month'>Month</option>
        <option value='week'>Week</option>
        <option value='day'>Day</option>
        <option value='agenda'>Agenda</option>
      </select>
    </header>
  );
};

URL sync

Keep view and date in the URL, so links and the back button work:

use-url-calendar.ts
const fromUrl = () => {
  const params = new URLSearchParams(window.location.search);
  const view = params.get('view');
  const date = params.get('date');

  return {
    view: isViewKey(view) ? view : 'week',
    date: date ? Temporal.PlainDate.from(date) : Temporal.Now.plainDateISO(),
  };
};

export const useUrlCalendar = () => {
  const [{ view, date }, setNavigation] = useState(fromUrl);

  return useCalendar({
    timeZone,
    views,
    features,
    view,
    date,
    events: bookings,
    onNavigate: next => {
      // `next.range` is known here, before rendering: start fetching its events now
      window.history.replaceState(null, '', `?view=${next.view}&date=${next.date.toString()}`);
      setNavigation({ view: next.view, date: next.date });
    },
  });
};

Range and period

Every view and date resolve to three inclusive date ranges on state.context:

  • period is the nominal span: a calendar period, months months, weeks whole weeks, or days days. Use it for titles.
  • range is what is visible. Month-based day grids extend to whole weeks; other views use the period itself. Hidden dates stay inside this anchor envelope.
  • inputRange covers the source dates the visible windows need, including the following morning for overnight windows. Fetch events for it.
ViewPeriodRange
Day grid, month-based durationThe calendar period or month countPadded to full weeks; six weeks per month with fixedWeekCount: true
Day grid, weeksweeks whole weeks from the week of dateSame as period
Time grid or list, days: 7The week containing dateSame as period
Time grid or list, otherdays days starting at dateSame as period

All four factories support days, weeks, months, and calendar period settings. Calendar quarters begin in January, April, July and October. A months: 3 period starts with the anchor month instead.

export const periodsFeatures = calendarFeatures({});
export const periods = calendarViews({
  features: periodsFeatures,
  views: {
    workweek: timeGridView({ weeks: 1, hiddenDays: [6, 7] }),
    threeMonths: listView({ months: 3 }),
    quarter: listView({ period: 'quarter' }),
    monthTimeline: timelineView({ period: 'month', groupBy: 'resource', slotMinutes: 1440, minSlotWidth: '6rem' }),
    yearTimeline: timelineView({ period: 'year', slotMinutes: 1440, minSlotWidth: '6rem' }),
  },
});

Weeks start on the firstDayOfWeek option, an ISO weekday from 1 (Monday) to 7 (Sunday, the default). The DayOfWeek constant names them (DayOfWeek.Monday is 1) and also fits hiddenDays and weekly background daysOfWeek; plain numbers stay valid. The calendar filters input by inputRange, and layouts then clip to visible dates and hours.

To get the range for any view and date without a calendar, for example to fetch on the server, use getCalendarNavigation:

import { getCalendarNavigation } from '@calendar-tsx/core';

const { range, period, inputRange } = getCalendarNavigation(views, 'month', Temporal.PlainDate.from('2026-03-11'), {
  firstDayOfWeek: 1,
});