calendar-tsx

Week and day view

A time grid with an all-day row and overlap lanes.

Days as columns on a time axis, with timed events placed by time and overlapping events side by side or cascaded. All-day events sit in a row above.

Loading the preview…

Live, with sample events. Click an event to see what it reports.
import { timeGridView } from '@calendar-tsx/core';

timeGridView({ days: 7 }); // a week
timeGridView({ days: 1 }); // a day
timeGridView({ weeks: 1, hiddenDays: [6, 7] }); // a workweek

Settings

Every view factory takes exactly one of days, weeks, months or period. Counts are positive integers.

DurationRangenext() / prev()
days: NN dates from date; 7 keeps week alignment.N days.
weeks: NN whole weeks from firstDayOfWeek.N weeks, keeping the weekday.
months: NN calendar months from the first of the anchor month.N months, landing on the first day.
period'month', 'quarter' or 'year', aligned to its calendar boundary.One period, landing on its first day.
SettingTypeDefaultDescription
allDaybooleantruefalse drops the all-day row.
slotMinutesnumber30Positive integer slot length; the final slot may be shorter.
minColumnWidthstringnonePassed through for horizontal scrolling, for example '8rem'.
groupBy'resource' or ['resource']noneOne column per resource and day. See resource view.
groupOrder'resource-first' | 'date-first''resource-first'Column order when grouped.
overlap'side-by-side' | 'cascade''side-by-side'How overlapping events share a column. See overlap.
minEventMinutesnumberhalf of slotMinutesShorter events are laid out this long. 0 turns it off.
shortEventMinutesnumber30Events laid out shorter than this are flagged isShort.
hiddenDaysISO weekdaysnoneSee hidden weekdays.
timeWindow{ startMinutes, endMinutes }[0, 1440)See visible hours.

Overlap

Each overlapping event takes the lowest lane free at its start, then widens to the right until the next lane holding an event it overlaps, or to the column's edge. 'side-by-side' keeps events beside each other. 'cascade' doubles each event's width, up to the column's edge, so a later lane covers at most half of it; later lanes paint on top.

Brief events

The core lays out an event shorter than minEventMinutes (half a slot by default) as that long, and gives it its own lane if it would then cover the next event. The event keeps its real end, so label it from segment.event. Both settings are minutes, measured on the displayed clock: half a slot is 24px with the default 3rem slots and 16px with compact.css, before the gap between events.

An event laid out shorter than shortEventMinutes gets isShort and data-short. The default theme puts its content on one line, and hides the content of an event too short for one line.

Hidden weekdays

hiddenDays omits ISO weekdays. Use the exported DayOfWeek constants (DayOfWeek.Monday is 1, DayOfWeek.Sunday is 7) or the numbers; the same values work for firstDayOfWeek and weekly background daysOfWeek.

import { DayOfWeek, timeGridView } from '@calendar-tsx/core';

timeGridView({ weeks: 1, hiddenDays: [DayOfWeek.Saturday, DayOfWeek.Sunday] }); // a workweek

Hidden weekdays change neither the nominal period, the inclusive fetch range nor the navigation step. Hiding all seven weekdays throws. When every date in the range is hidden, such as a one-day view on a hidden Saturday, the range shows its dates anyway.

Visible hours

timeWindow limits the clock hours shown, in integer clock minutes: 0 <= startMinutes < 1440 and startMinutes < endMinutes <= startMinutes + 1440. The default is [0, 1440).

timeGridView({ days: 7, timeWindow: { startMinutes: 480, endMinutes: 1080 } }); // 08:00 to 18:00
timelineView({ days: 1, timeWindow: { startMinutes: 1320, endMinutes: 1800 } }); // 22:00 to 06:00

An overnight window belongs to its starting date, so hiding Tuesday still shows Monday's Tuesday-morning portion. All-day items keep their calendar dates. slotMinutes need not divide the window; the final slot keeps its actual size, and each slot's dayOffset labels after-midnight times. Fetch events for navigation's inputRange, which adds the following date when an overnight window needs it.

Example

week-view.tsx
export const Week = ({ calendar }: { calendar: DayCalendar }) => {
  const grid = useCalendarSelector((state: State) => state.layout);

  if (grid.kind !== 'timeGrid') return null;

  return (
    <TimeGridView model={grid}>
      <TimeGridViewHeader>
        {grid.columns.map(column => (
          <TimeGridViewColumnHeader key={column.key} column={column}>
            <TimeGridViewColumnTrigger calendar={calendar} column={column} view='day'>
              {column.date.toLocaleString('en-US', { weekday: 'short', day: 'numeric' })}
            </TimeGridViewColumnTrigger>
          </TimeGridViewColumnHeader>
        ))}
      </TimeGridViewHeader>
      {grid.allDay && (
        <TimeGridViewAllDay>
          {/* before the bars: grid items paint in DOM order */}
          {grid.allDay.backgroundEvents.map(segment => (
            <TimeGridViewAllDayBackgroundEvent key={segment.key} segment={segment} />
          ))}
          {grid.allDay.segments.map(segment => (
            <TimeGridViewAllDayEvent key={segment.key} segment={segment}>
              {segment.event.data.title}
            </TimeGridViewAllDayEvent>
          ))}
        </TimeGridViewAllDay>
      )}
      <TimeGridViewBody model={grid}>
        <TimeGridViewAxis>
          {grid.slots.map(slot => (
            <TimeGridViewAxisLabel key={slot.key} slot={slot}>
              {slot.time.minute === 0 ? slot.time.toLocaleString('en-US', { hour: 'numeric' }) : null}
            </TimeGridViewAxisLabel>
          ))}
        </TimeGridViewAxis>
        {grid.columns.map(column => (
          <TimeGridViewColumn key={column.key} column={column}>
            {column.backgroundEvents.map(segment => (
              <TimeGridViewBackgroundEvent key={segment.key} segment={segment} />
            ))}
            {column.segments.map(segment => (
              <TimeGridViewEvent key={segment.key} segment={segment}>
                {segment.event.data.title}
              </TimeGridViewEvent>
            ))}
            <TimeGridViewNowIndicator column={column} />
          </TimeGridViewColumn>
        ))}
      </TimeGridViewBody>
    </TimeGridView>
  );
};

Model

layout is { kind: 'timeGrid', coordinates, slotUnits, slotMinutes, hourOffset, slots, columns, allDay, minColumnWidth }.

  • slots holds { key, time, dayOffset, start, span } for axis labels, as fractions of the visible window.
  • slotMinutes is each slot's length, and hourOffset the slot units from the window's start to its first hour (0 on the hour). Themes draw hour lines from them.
  • columns holds one column per day.
  • allDay holds bar segments, backgroundEvents and rows, with columns indexed like columns. Its segments are the day grid's bar segments. It is absent when the view sets allDay: false.

Positions inside a column are fractions of its visible clock window.

Time grids and timelines use a fixed clock axis in timeZone: 09:00 sits at the 09:00 position on daylight-saving days too, and missing hours stay on the axis. The two occurrences of a repeated hour share clock positions; overlapping events then take separate lanes or stack rows. A bar that crosses a transition encloses every clock position its real interval visits, and segment.event keeps the real timestamps and offsets. Each timed segment's fragments keeps its exact coverage (offsets, occurrences and the reason each fragment stops) apart from that enclosing rectangle.

The core builds one coordinate plan (layout.coordinates) per stable view definition and reuses it across event, resource and clock updates. Navigation, timeZone, firstDayOfWeek, hiddenDays or timeWindow changes rebuild it.

Column fieldDescription
dateThe day.
isTodayThe day is today in timeZone.
segmentsTimed events in this column.
lanesThe most lanes any overlap cluster in the column uses.
backgroundEventsSee background events.
nowFraction of the window containing now, including overnight columns.
resource, isUnavailable, unavailableSee resource view.
Timed segment fieldDescription
eventThe event, in the calendar's time zone.
start, spanFractions of the visible window.
fragmentsExact coverage before clock enclosure and minimum sizing.
lane, lanesIts lane, and the lane count of its overlap cluster.
inlineStart, inlineSizeFractions of the column's width, after widening and any cascade.
isShortLaid out shorter than shortEventMinutes.
continuesBefore, continuesAfterThe event is clipped at a window boundary.

Events that overlap, directly or through others, form a cluster that shares one lane count. Events that only touch do not overlap. Place events with inlineStart and inlineSize; lane and lanes remain for CSS written against them.

Parts

PartModel propElementNotes
TimeGridViewmodeldivrole='region', labelled "Time grid". Scrolls.
TimeGridViewHeadernonedivSticky column headers.
TimeGridViewColumnHeadercolumndivdata-today, data-weekday, data-unavailable, data-resource-id.
TimeGridViewColumnTriggercalendar, column, viewbuttonNavigates through calendar to view on the column's date.
TimeGridViewAllDaynonedivRender it only when allDay is set.
TimeGridViewAllDayEventsegmentbuttonA bar segment; data-all-day, data-multi-day.
TimeGridViewAllDayBackgroundEventsegmentdivaria-hidden. Render it before the bars.
TimeGridViewBodymodeldivHolds the axis and the columns. Emits --cal-slot-units for the grid height.
TimeGridViewAxisnonedivOptional.
TimeGridViewAxisLabelslotdivslot is the time slot, not the HTML attribute. Emits --cal-start, --cal-span.
TimeGridViewColumncolumndivdata-today, data-weekday, data-unavailable.
TimeGridViewBackgroundEventsegmentdivaria-hidden. Render it before the events.
TimeGridViewBackgroundbackgrounddivaria-hidden. A background layer's shading.
TimeGridViewEventsegmentbuttonPlaced by time and lane; data-overlap when it shares lanes, data-short when isShort.
TimeGridViewNowIndicatorcolumndivaria-hidden. Renders only in today's column.

Give the root a height, such as max-block-size: 40rem, to scroll inside the page instead of showing all 24 hours.