Business Calendar
import { BusinessCalendar, createBusinessCalendar } from '@absoluteui/components/business-calendar';import '@absoluteui/components/business-calendar/style.css';Using Angular? See Business Calendar for Angular.
What it does
Section titled “What it does”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.
When to use
Section titled “When to use”- 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.
When not to use
Section titled “When not to use”- 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.
Examples
Section titled “Examples”Office calendar with holidays, editable
Section titled “Office calendar with holidays, editable”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);});Working-day arithmetic without UI
Section titled “Working-day arithmetic without UI”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 };Show a year by default
Section titled “Show a year by default”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)); });}More usage
Section titled “More usage”See section 33 and metadata/business-calendar/examples.json: editable office calendar, engine-only arithmetic, global configuration.
Anatomy
Section titled “Anatomy”<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">...API reference
Section titled “API reference”Factory
Section titled “Factory”BusinessCalendar.create(host: HTMLElement, options: BusinessCalendarOptions): BusinessCalendarGlobal configuration key: businessCalendar (see Configuration).
Options
Section titled “Options”| 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. |
Properties
Section titled “Properties”| 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. |
Methods
Section titled “Methods”| 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. |
Events
Section titled “Events”| 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. |
CSS classes
Section titled “CSS classes”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. |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
previous |
chevron-left |
next |
chevron-right |
Design tokens
Section titled “Design tokens”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
TypeScript definitions
Section titled “TypeScript definitions”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 }Accessibility
Section titled “Accessibility”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.
Keyboard
Section titled “Keyboard”| 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 |
ARIA attributes
Section titled “ARIA attributes”| 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.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”| 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.
Notes and known caveats
Section titled “Notes and known caveats”- 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.
Behavior
Section titled “Behavior”States
Section titled “States”Working, non-working, holiday, changed (exception), today, focused, editable, read-only.
State transitions
Section titled “State transitions”| 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 |
Accessibility notes
Section titled “Accessibility notes”- 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.
Responsive behavior
Section titled “Responsive behavior”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.
Right-to-left
Section titled “Right-to-left”direction: 'rtl' mirrors the grids and chevrons; ArrowLeft moves to the next day.
Theming
Section titled “Theming”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.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { businessCalendar: { months: 12, showHolidays: false } } });Global options: months, showLegend, showHolidays, plus inherited locale, timezone, and direction.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults; all options are dynamic through update().
Timezones
Section titled “Timezones”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.
Security
Section titled “Security”Holiday names and reasons render through textContent (and title). Definitions are validated; no HTML sinks; CSP-safe.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. Edits change the in-memory definition (definition getter) and are reported by aui-change.
Performance
Section titled “Performance”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.
Supported data scale
Section titled “Supported data scale”Up to 12 visible months and a few hundred holidays and exceptions.
Edge cases
Section titled “Edge cases”- 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; usenextWorkingDayto move to one.
Errors
Section titled “Errors”| 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.
Known limitations
Section titled “Known limitations”- One hours pattern per weekday; no rotating shifts.
- Movable holidays must be listed per year.
- Editing toggles working state only.
- Timezone accuracy of
workingIntervalsdepends on the browser ICU data. - Manual screen-reader verification is pending.
Unsupported scenarios
Section titled “Unsupported scenarios”- HTML in holiday names.
- Range selection.
- Several calendars merged in one view.