Skip to content

Business Calendar

Date and TimeSince 0.6.0
import { BusinessCalendar, createBusinessCalendar } from '@absoluteui/components/business-calendar';
import '@absoluteui/components/business-calendar/style.css';

Using Angular? See Business Calendar for Angular.

Defines and shows which days count as working time. A BusinessCalendarDefinition lists working weekdays (default Monday to Friday), working hours (default 09:00 to 17:00, optionally per weekday), holidays (one-off or yearly), and exceptions (a date made working or non-working, with optional hours and reason).

BusinessCalendar shows one to twelve months of that definition: working days, non-working days (weekends), holidays, and changed days, with a legend and a list of the holidays in view. In edit mode Enter, Space, or a click toggles a day between working and non-working; the updated definition is emitted for saving. The same subpath exports the pure engine createBusinessCalendar, used by Scheduler (work weeks, business-hours shading, holiday headers) and by application code for working-day arithmetic.

  • Maintaining office days, public holidays, and one-off closures or extra working days.
  • Showing a team or site which days count as working days.
  • Working-day arithmetic in code (due dates, SLAs) with the engine alone.
  • Picking a date: use Date Picker or Calendar.
  • Scheduling events: use Scheduler (it reads the same definition for business hours).
  • Per-person availability or shifts: not modelled; a definition describes one calendar.
import { plainDate, plainYearMonth } from '@absoluteui/core';
import { BusinessCalendar, type BusinessCalendarChangeDetail } from '@absoluteui/components/business-calendar';
const host = document.createElement('div');
document.body.append(host);
BusinessCalendar.create(host, {
label: 'London office',
month: plainYearMonth(2026, 12),
months: 2,
editable: true,
definition: {
holidays: [
{ date: plainDate(2026, 12, 25), name: 'Christmas Day', yearly: true },
{ date: plainDate(2026, 12, 28), name: 'Boxing Day (substitute)' },
],
},
});
host.addEventListener('aui-change', (event) => {
const { definition } = (event as CustomEvent<BusinessCalendarChangeDetail>).detail;
host.dataset.exceptions = String(definition.exceptions?.length ?? 0);
});
import { plainDate, plainTime } from '@absoluteui/core';
import { createBusinessCalendar } from '@absoluteui/components/business-calendar';
const calendar = createBusinessCalendar({
workingDays: [1, 2, 3, 4, 5],
weekdayHours: { '5': [{ start: plainTime(9), end: plainTime(13) }] },
holidays: [{ date: plainDate(2026, 1, 1), name: "New Year's Day", yearly: true }],
});
const due = calendar.addWorkingDays(plainDate(2025, 12, 30), 3);
const days = calendar.workingDaysBetween(plainDate(2026, 1, 1), plainDate(2026, 2, 1));
const hours = calendar.workingIntervals(plainDate(2026, 1, 2), 'Europe/London');
export const summary = { due, days, hours };
import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { businessCalendar: { months: 12, showHolidays: false } } });
import { plainDate, plainYearMonth } from '@absoluteui/core';
import { BusinessCalendar, type BusinessCalendarChangeDetail } from '@absoluteui/components/business-calendar';
import '@absoluteui/components/business-calendar/style.css';
const host = document.querySelector<HTMLElement>('#office-days');
if (host) {
BusinessCalendar.create(host, {
label: 'London office',
month: plainYearMonth(2026, 12),
editable: true,
definition: { holidays: [{ date: plainDate(2026, 12, 25), name: 'Christmas Day', yearly: true }] },
});
host.addEventListener('aui-change', (event) => {
const { definition } = (event as CustomEvent<BusinessCalendarChangeDetail>).detail;
localStorage.setItem('office-days', JSON.stringify(definition));
});
}

See section 33 and metadata/business-calendar/examples.json: editable office calendar, engine-only arithmetic, global configuration.

<div class="aui-business-calendar [--editable]"> (consumer host)
<div class="aui-business-calendar__header">
<button class="aui-business-calendar__nav aui-business-calendar__prev" aria-label="Previous months">
<div class="aui-business-calendar__range" aria-live="polite">January 2026 to March 2026</div>
<button class="aui-business-calendar__nav aui-business-calendar__next" aria-label="Next months">
<div class="aui-business-calendar__months">
<div class="aui-business-calendar__month"> (one per month)
<h3 class="aui-business-calendar__heading">January 2026</h3>
<table class="aui-business-calendar__grid" role="grid" aria-label="{label}, January 2026" [aria-readonly="true"]>
<thead><tr><th class="aui-business-calendar__weekday" scope="col" abbr="Monday">Mon</th>...
<tbody><tr><td class="aui-business-calendar__day --working|--non-working [--holiday] [--changed] [--today]"
aria-label="Thursday, January 1, 2026, holiday: New Year's Day" tabindex="-1|0">
<span class="aui-business-calendar__number" aria-hidden="true">1</span>
<ul class="aui-business-calendar__legend" aria-label="Legend">...
<div class="aui-business-calendar__holidays"><h3>Holidays</h3><ul class="aui-business-calendar__holiday-list">...
BusinessCalendar.create(host: HTMLElement, options: BusinessCalendarOptions): BusinessCalendar

Global configuration key: businessCalendar (see Configuration).

Option Type Default Description
label string Accessible name; each grid is named “{label}, {month year}”. Required.
definition BusinessCalendarDefinition {} Working days (default Monday to Friday), hours (default 09:00 to 17:00), weekday hours, holidays, and exceptions. Validated (BUSINESS_CALENDAR_DEFINITION).
month PlainYearMonth First visible month. Default: the month of the focused date.
focusedDate PlainDate Initially focused day. Default: today.
editable boolean false Enter, Space, or a click toggles a day between working and non-working (an exception).
firstDayOfWeek IsoWeekday First column (1 = Monday). Default: the locale’s first day of week.
today PlainDate Advanced: fixed “today” for tests and demos.
icons Partial<{ previous: IconRef; next: IconRef }> Icon slot overrides.
months number 3 Consecutive months shown, 1 to 12; the previous/next buttons page by this many. Globally configurable.
showLegend boolean true Show the legend of day kinds. Globally configurable.
showHolidays boolean true List the holidays of the visible months. Globally configurable.
locale string "en-US" Month and weekday names, dates, numbers, the first day of the week, and messages. Globally configurable. Inherited from common configuration.
timezone string "UTC" IANA timezone of “today” (never the host machine zone). Globally configurable. Inherited from common configuration.
direction 'ltr' | 'rtl' "ltr" Reading direction; Left/Right arrow keys and chevrons follow it. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement Consumer host; its children are owned by the component.
definition BusinessCalendarDefinition The current definition including edits (for saving).
engine BusinessCalendarEngine The engine of the current definition (day, isWorkingDay, addWorkingDays, workingDaysBetween, nextWorkingDay, previousWorkingDay, workingIntervals, isWorkingTime).
focusedDate PlainDate The day holding the roving tab stop.
visibleMonth PlainYearMonth The first visible month.
options Readonly<BusinessCalendarOptions> Current options.
Method Description
update(options: Partial<BusinessCalendarOptions>): void Merge and apply options; a definition replaces the current one (and its edits).
goTo(date: PlainDate): void Show a date and make it the focused day (no events).
setWorking(date: PlainDate, working: boolean): void Programmatic edit (no events): adds, or removes when redundant, an exception.
focus(): void Focus the focused day.
destroy(): void Remove rendering, listeners, and subscriptions.
createBusinessCalendar(definition?: BusinessCalendarDefinition): BusinessCalendarEngine Module function: a validated engine without UI.
Event Detail Bubbles Cancelable Description
aui-change { date: PlainDate; working: boolean; definition: BusinessCalendarDefinition } Yes No Edit mode: the user toggled a day; definition is the updated definition.
aui-day-select { date: PlainDate; day: BusinessDay } Yes No The user activated a day (click, Enter, or Space), in both modes.
aui-navigate { month: PlainYearMonth } Yes No The visible months changed by user action.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-business-calendar | --editable Host and edit-mode modifier.
aui-business-calendar__header | __nav | __prev | __next | __range Paging header and the visible range (live region).
aui-business-calendar__months | __month | __heading | __grid | __weekday Month panels and their grids.
aui-business-calendar__day | --working | --non-working | --holiday | --changed | --today | --empty Day cells and their kinds.
aui-business-calendar__legend | __legend-item | __swatch Legend.
aui-business-calendar__holidays | __holidays-heading | __holiday-list Holidays in view.
Slot Default icon
previous chevron-left
next chevron-right

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-border-width-strong | --aui-color-border | --aui-color-focus-ring | --aui-color-hover | --aui-color-info-fg | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-muted | --aui-color-warning-fg | --aui-color-warning-subtle | --aui-control-height-sm | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-size-xs | --aui-font-weight-bold | --aui-font-weight-medium | --aui-font-weight-semibold | --aui-icon-size-sm | --aui-line-height-normal | --aui-radius-md | --aui-radius-sm | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-space-4 | --aui-touch-target-min

interface WorkingHours { start: PlainTime; end: PlainTime }
interface BusinessHoliday { date: PlainDate; name: string; yearly?: boolean }
interface BusinessDayException { date: PlainDate; working: boolean; hours?: readonly WorkingHours[]; name?: string }
interface BusinessCalendarDefinition { workingDays?: readonly IsoWeekday[]; hours?: readonly WorkingHours[]; weekdayHours?: Partial<Record<'1' | '2' | '3' | '4' | '5' | '6' | '7', readonly WorkingHours[]>>; holidays?: readonly BusinessHoliday[]; exceptions?: readonly BusinessDayException[] }
interface BusinessDay { date: PlainDate; working: boolean; hours: readonly WorkingHours[]; holiday: string | null; exception: boolean; reason: string | null; weekend: boolean }
interface BusinessCalendarChangeDetail { date: PlainDate; working: boolean; definition: BusinessCalendarDefinition }
interface BusinessCalendarDaySelectDetail { date: PlainDate; day: BusinessDay }
interface BusinessCalendarNavigateDetail { month: PlainYearMonth }

Pattern: WAI-ARIA APG grid (as Calendar): one table role=grid per month with a single roving tab stop across all months; every day cell names its date and kind.

Key Context Action
ArrowLeft / ArrowRight Day Previous / next day (mirrored in RTL); pages when leaving the visible months.
ArrowUp / ArrowDown Day Same weekday of the previous / next week.
Home / End Day First / last day of the week.
PageUp / PageDown Day Same day of the previous / next month; with Shift, the previous / next year.
Enter / Space Day Emits aui-day-select; in edit mode also toggles working / non-working and announces the change.
Tab Calendar Previous button, next button, then the focused day (one tab stop for all months).
Element Role
month table grid
day cell gridcell
weekday header columnheader
visible range none (aria-live=polite)
legend and holidays list
Attribute Element Value
aria-label grid “{label}, {month year}”
aria-label day cell Full date and kind, for example “Thursday, January 1, 2026, holiday: New Year’s Day” or “…, working day (changed): Stocktaking”
aria-current day cell “date” on today
aria-readonly grid “true” unless editable
aria-label previous/next buttons “Previous months” / “Next months” (singular with one month)

One roving tab stop across all visible months; moving past the visible months pages them. Focus stays on a toggled day after an edit.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
4.1.2 Name, Role, Value A
4.1.3 Status Messages AA

Automated test results: Accessibility and compliance.

  • Day kinds are never conveyed by color alone: each cell names its kind, holidays have a bottom bar, changed days a dashed outline, and holidays are listed as text.
  • Edits are announced through the shared live region (“January 6, 2026 is now a non-working day.”).
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.

Working, non-working, holiday, changed (exception), today, focused, editable, read-only.

From Trigger To
working day (edit mode) Enter, Space, click, setWorking(date, false) non-working (changed); aui-change (user)
changed day toggle again exception removed (pattern restored)
holiday (edit mode) toggle working (changed), the holiday name kept
months shown previous/next, PageUp/PageDown, arrows past the edge other months; aui-navigate
  • Kinds never rely on color: names state them, holidays have a bottom bar and are listed as text, changed days have a dashed outline.
  • Day numbers are aria-hidden; the cell name includes the full date.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.

Months flow into as many columns as fit (auto-fill grid, 15rem minimum); at 320 px one month per row without horizontal scrolling. The legend wraps. Navigation buttons are touch-target-min squares.

direction: 'rtl' mirrors the grids and chevrons; ArrowLeft moves to the next day.

Working days: surface with a border. Non-working: sunken surface, muted text. Holidays: subtle warning fill and a warning-colored bottom bar. Changed days: dashed info-colored outline. Forced colors: CanvasText borders and bars, GrayText for non-working days, Highlight focus.

configureAbsoluteUI({ components: { businessCalendar: { months: 12, showHolidays: false } } });

Global options: months, showLegend, showHolidays, plus inherited locale, timezone, and direction.

Instance options override global and theme defaults; all options are dynamic through update().

Dates are PlainDate without a timezone. “Today” is the current date in the configured timezone (UTC unless configured), never the host zone. The engine converts working hours into instants only with an explicit zone and DST policy: workingIntervals(date, 'Europe/London') resolves wall-clock hours through the timezone engine (a day with a DST change has correct instants). isWorkingTime(instant, zone) reads the wall clock in that zone.

previous (chevron-left) and next (chevron-right); swapped in RTL.

Holiday names and reasons render through textContent (and title). Definitions are validated; no HTML sinks; CSP-safe.

No persistence, network access, or telemetry. Edits change the in-memory definition (definition getter) and are reported by aui-change.

Rendering creates at most 12 month tables (about 370 cells). The engine caches day lookups (up to 4,000 dates). Working-day arithmetic and counting use @absoluteui/helpers/business-calendar: counts do not depend on the length of the range (full weeks are counted arithmetically, holidays by binary search) and are exact for any range. Budget (e2e): create with 12 months < 40 ms.

Up to 12 visible months and a few hundred holidays and exceptions.

  • A holiday on a weekend is shown as a holiday; the day stays non-working.
  • An exception wins over holidays and the pattern; toggling a changed day back removes the exception when the pattern already gives that state.
  • Yearly holidays on 29 February occur only in leap years.
  • Working hours ending at 00:00 end at midnight (the next day’s start).
  • addWorkingDays(date, 0) returns the date unchanged, even on a non-working day; use nextWorkingDay to move to one.
Code Category Cause
BUSINESS_CALENDAR_OPTIONS configuration Empty label; invalid month, focusedDate, today, editable, firstDayOfWeek, or config options.
BUSINESS_CALENDAR_DEFINITION configuration Invalid weekdays, overlapping or reversed hours, holidays without a name, two exceptions on one date, wrong types.
BUSINESS_CALENDAR_NO_WORKING_DAYS runtime The calendar has no working day at all (for example workingDays: [] without working exceptions).

A failed construction leaves the host untouched.

  • One hours pattern per weekday; no rotating shifts.
  • Movable holidays must be listed per year.
  • Editing toggles working state only.
  • Timezone accuracy of workingIntervals depends on the browser ICU data.
  • Manual screen-reader verification is pending.
  • HTML in holiday names.
  • Range selection.
  • Several calendars merged in one view.