calendar-tsx

Background layers

Shade business hours, holidays and other rules in the time grid and day-grid views.

backgroundFeature(layers) shades time from rules, such as business hours or holidays, in the time-grid, day-grid and timeline views. Each layer passed to the factory is one kind of shading: fixed ranges, ranges computed for the visible period, or the time they leave uncovered.

// module scope: layers and their ranges functions are compared by reference
export const backgrounds = [
  createBackgroundLayer({
    id: 'business-hours',
    kind: 'closed',
    inverse: true,
    ranges: createWeeklyRanges([
      {
        daysOfWeek: [1, 2, 3, 4, 5],
        startTime: Temporal.PlainTime.from('09:00'),
        endTime: Temporal.PlainTime.from('17:00'),
      },
    ]),
  }),
  createBackgroundLayer({
    id: 'holidays',
    kind: 'holiday',
    ranges: [{ allDay: true, start: Temporal.PlainDate.from('2026-12-25') }],
  }),
];

Register the configured feature, then bind it to the view definitions:

export const features = calendarFeatures({ background: backgroundFeature(backgrounds) });
export const views = calendarViews({
  features,
  views: {
    week: timeGridView({ days: 7 }),
    month: dayGridView({ period: 'month', maxEventsPerDay: 3 }),
    timeline: timelineView({ days: 7, groupBy: 'resource' }),
  },
});

export const calendar = createCalendar({
  timeZone,
  views,
  features,
  view: 'week',
  date: Temporal.PlainDate.from('2026-03-11'),
  events: bookings,
  resources: rooms,
  onNavigate: next => calendar['~update']({ ...calendar.options, view: next.view, date: next.date }),
});

For dated items from your data, such as one holiday with its own id and payload, use background events instead. Both can shade the same view.

Layers

FieldTypeDescription
idstringUnique; emitted as data-layer.
kindstringEmitted as data-kind; style by it.
rangesCalendar_BackgroundRange[] or a functionWhat the layer shades. A function receives the visible range and the timeZone.
inversebooleanShade what the ranges leave uncovered instead.
resourceIdsstring[]Shade only these resources' columns or timeline rows. Omitted, it shades every resource.

A range is timed, { start, end } as Temporal.ZonedDateTimes, or all-day, { allDay: true, start, end? } as Temporal.PlainDates with an inclusive end.

  • createBackgroundLayer returns its argument. It types the layer where you write it and keeps kind a literal.
  • createWeeklyRanges(rules) returns a ranges function from weekly opening hours. Each rule takes ISO daysOfWeek (1 Monday to 7 Sunday), a startTime and an endTime. An endTime at or before the startTime ends the next day, so 22:00 to 02:00 works.
  • Keep backgrounds and every ranges function at module scope or memoized. Factory defaults are fixed at calendar creation. To change a view override, replace its view definition and the views map.
  • An inverse layer with no ranges shades everything, the same rule as a resource with no availability.

Parts

PartModel propWhere
TimeGridViewBackgroundbackgroundIn TimeGridViewColumn, before the events.
DayGridViewDayBackgroundbackgroundIn DayGridViewDay, before its content.
TimelineViewBackgroundbackgroundIn TimelineViewLane, beneath foreground events.

Each renders an aria-hidden div with data-layer and data-kind. A label such as "Closed" is your own text.

backgrounds.tsx
// in the time grid: under the column's events
export const Column = ({ column }: { column: Grid['columns'][number] }) => (
  <TimeGridViewColumn column={column}>
    {column.backgrounds.map(background => (
      <TimeGridViewBackground key={background.key} background={background} />
    ))}
    {/* background events, events and the now indicator as usual */}
  </TimeGridViewColumn>
);

// in the month view: inside the day, beneath its content
export const Day = ({ day }: { day: Month['sections'][number]['rows'][number]['days'][number] }) => (
  <DayGridViewDay day={day}>
    {day.backgrounds.map(background => (
      <DayGridViewDayBackground key={background.key} background={background} />
    ))}
    {day.date.day}
  </DayGridViewDay>
);

Model

ViewFieldContents
Time gridcolumn.backgrounds{ key, layer, kind, start, span }, as fractions of the day.
Day gridday.backgrounds{ key, layer, kind } for each layer that shades the day.
Timelinerow.backgrounds{ key, layer, kind, start, span }, as fractions of the visible track.
  • In the time grid, an all-day range shades the whole column. Overlapping ranges within a layer merge; layers stack in input order.
  • In the day grid, a day takes a layer when one of the layer's all-day ranges covers it. Timed ranges don't shade day-grid cells, except through inverse layers: an inverse layer marks the days none of its ranges touch, so business hours mark the weekends.
  • In timelines, all-day ranges cover anchor windows, including overnight portions. Timed ranges use exact source coverage, preserving DST gaps and combining repeated hours. Coverage merges before inverse gaps are computed. Hidden weekdays contribute no shading; range callbacks receive inputRange.
  • Resource-limited layers skip rows or columns without a matching resource, including ungrouped timelines.
  • Every supported view inherits the registered background feature. Per-view arrays replace the default layers. The time grid's all-day row and the list aren't shaded.