Date Range Picker
import { DateRangePicker } from '@absoluteui/components/date-range-picker';import '@absoluteui/components/date-input/style.css';Using Angular? See Date Range Picker for Angular.
What it does
Section titled “What it does”An inclusive date range field with a calendar popup (WAI-ARIA APG date picker dialog, non-modal variant). DateRangePicker composes two Date Inputs (start and end, date granularity) inside one labelled role="group", plus a calendar button inside the end field; the button opens a range-selecting Calendar (one or two months) in a role="dialog" popup in the top layer. The value is a Range<PlainDate>, set only when both dates are valid and every range constraint passes. Typing and picking produce the same value. The picker, not its inner Date Inputs, is the form control that Form discovers.
When to use
Section titled “When to use”- Inclusive date ranges such as stays, trips, leave requests, and report periods.
- Ranges with bounds, unavailable days, or a minimum or maximum length in days.
When not to use
Section titled “When not to use”- A single date: use Date Picker.
- Two unrelated dates that do not form a range: use two Date Pickers.
- Ranges of exact moments with a time of day: not supported (use two Date Inputs with minute granularity).
- An always-visible range calendar: use Calendar with selectionMode ‘range’.
Examples
Section titled “Examples”Required stay with a length limit
Section titled “Required stay with a length limit”import { plainDate, plainDateToString } from '@absoluteui/core';import { DateRangePicker, type DateRangePickerChangeDetail } from '@absoluteui/components/date-range-picker';
const host = document.createElement('div');document.body.append(host);DateRangePicker.create(host, { label: 'Stay', startLabel: 'Check-in', endLabel: 'Check-out', name: 'stay', required: true, min: plainDate(2026, 1, 1), minDays: 2, maxDays: 14,});host.addEventListener('aui-change', (event) => { const { value } = (event as CustomEvent<DateRangePickerChangeDetail>).detail; host.dataset.value = value ? `${plainDateToString(value.start)}/${plainDateToString(value.end)}` : '';});Initial range, weekdays only, and a consumer rule
Section titled “Initial range, weekdays only, and a consumer rule”import { dayOfWeek, plainDate } from '@absoluteui/core';import { DateRangePicker } from '@absoluteui/components/date-range-picker';
const host = document.createElement('div');document.body.append(host);DateRangePicker.create(host, { label: 'Leave', value: { start: plainDate(2026, 10, 5), end: plainDate(2026, 10, 9) }, startName: 'leaveFrom', endName: 'leaveTo', months: 1, firstDayOfWeek: 1, isDateDisabled: (date) => dayOfWeek(date) >= 6, validate: (range) => (range && range.start.month !== range.end.month ? 'Request leave within one month.' : null),});Always two months, and keep the popup open after picking
Section titled “Always two months, and keep the popup open after picking”import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ common: { timezone: 'Europe/Berlin' }, components: { dateRangePicker: { months: 2, closeOnSelect: false } } });import { dayOfWeek } from '@absoluteui/core';import { DateRangePicker } from '@absoluteui/components/date-range-picker';import '@absoluteui/components/date-input/style.css';import '@absoluteui/components/calendar/style.css';import '@absoluteui/components/date-range-picker/style.css';
const host = document.querySelector<HTMLElement>('#leave');if (host) { const picker = DateRangePicker.create(host, { label: 'Leave', name: 'leave', maxDays: 10, isDateDisabled: (d) => dayOfWeek(d) >= 6 }); host.addEventListener('aui-change', () => { if (!picker.validity.valid) picker.reportValidity(); });}More usage
Section titled “More usage”import { plainDate } from '@absoluteui/core';import { DateRangePicker } from '@absoluteui/components/date-range-picker';
const host = document.createElement('div');document.body.append(host);DateRangePicker.create(host, { label: 'Stay', name: 'stay', required: true, min: plainDate(2026, 1, 1), minDays: 2 });Anatomy
Section titled “Anatomy”<div class="aui-date-range-picker" data-aui-control> (consumer host; registered with Form) <div class="aui-date-range-picker__group" role="group" aria-labelledby=label aria-describedby=description> <div class="aui-date-range-picker__label" id>Stay</div> <div class="aui-date-range-picker__fields"> <div class="aui-date-range-picker__start aui-date-input"> (start Date Input; not registered) <div class="aui-date-input__label">Start date</div> (visually hidden) <div class="aui-date-input__group" role="group"> segments ... </div> <div class="aui-date-input__description"> <div class="aui-date-input__hint"> <div class="aui-date-input__error"> <input type="hidden" name=startName> </div> <span class="aui-date-range-picker__separator" aria-hidden="true">-</span> <div class="aui-date-range-picker__end aui-date-input"> (end Date Input; not registered) <div class="aui-date-input__label">End date</div> (visually hidden) <div class="aui-date-input__group" role="group"> <div class="aui-date-input__fields"> segments ... </div> <div class="aui-date-range-picker__trailing"> (Date Input `trailing` option) <button class="aui-date-range-picker__button" aria-haspopup="dialog" aria-expanded aria-controls aria-label="Choose dates"> <div class="aui-date-range-picker__popup" role="dialog" aria-modal="false" aria-label="Choose dates" popover="manual"> <div class="aui-calendar"> ... </div> (selectionMode 'range') </div> </div> </div> <div class="aui-date-input__description"> <div class="aui-date-input__hint"> <div class="aui-date-input__error"> <input type="hidden" name=endName> </div> </div> <div class="aui-date-range-picker__description" id hidden> <input type="hidden" name> (ISO 8601 interval) </div></div>The group label names both fields; each field keeps its own accessible name (“Start date”, “End date”, or startLabel/endLabel), visually hidden. The button and popup sit inside the end field’s group in the DOM (the popup renders in the top layer), so moving focus between the end segments, the button, and the calendar does not mark the end field touched, and a picker inside a modal Dialog stays inside the dialog’s subtree.
API reference
Section titled “API reference”Factory
Section titled “Factory”DateRangePicker.create(host: HTMLElement, options: DateRangePickerOptions): DateRangePickerGlobal configuration key: dateRangePicker (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
label |
string |
Visible group label and accessible name of the role=group around both fields. Required. | |
startLabel |
string |
Accessible name of the start field (visually hidden). Default: localized ‘Start date’. | |
endLabel |
string |
Accessible name of the end field (visually hidden). Default: localized ‘End date’. | |
value |
Range<PlainDate> | null |
null |
Initial inclusive range (start <= end); also the value restored by reset(). |
min |
PlainDate | null |
Inclusive lower bound for both dates (validation and calendar). | |
max |
PlainDate | null |
Inclusive upper bound for both dates (validation and calendar). | |
minDays |
number |
Minimum range length in days, counting the start and end day (integer 1-100000). | |
maxDays |
number |
Maximum range length in days, counting the start and end day (integer 1-100000). | |
name |
string |
Form field name; Form and the hidden input carry the ISO 8601 interval YYYY-MM-DD/YYYY-MM-DD. | |
startName |
string |
Field name of the start date’s own hidden input (YYYY-MM-DD) for native form posts. | |
endName |
string |
Field name of the end date’s own hidden input (YYYY-MM-DD) for native form posts. | |
description |
string |
Help text referenced by the group’s aria-describedby. | |
required |
boolean |
false |
Both dates are required (value-missing). |
disabled |
boolean |
false |
Disables both fields and the calendar button; a disabled picker is not submitted. |
readOnly |
boolean |
false |
Prevents editing; the calendar button is disabled. |
isDateDisabled |
(date: PlainDate) => boolean |
Disable days in the calendar; a typed disabled start or end day is invalid (‘date-unavailable’). Days between the ends are not checked. | |
businessCalendar |
BusinessCalendarEngine | null |
Business calendar (since 0.9.0): non-working days and holidays are marked and named in the calendar. | |
disableNonWorking |
boolean |
"false" |
With businessCalendar: non-working days cannot be picked or typed (since 0.9.0). |
validate |
(value: Range<PlainDate> | null) => string | null |
Consumer validation, run when the field and range constraints pass; return a message to mark the value invalid (error ‘custom’). | |
firstDayOfWeek |
1 | 2 | 3 | 4 | 5 | 6 | 7 |
First calendar column (ISO weekday). Default: the locale’s first day of week. | |
icons |
Partial<{ calendar: IconRef }> |
Icon slot overrides. | |
closeOnSelect |
boolean |
true |
Close the popup after the last day of the range is picked. Globally configurable. |
months |
'auto' | 1 | 2 |
"auto" |
Months in the popup. ‘auto’: two when the viewport is at least 40rem wide when the popup opens, else one. Applied on the next open. Globally configurable. |
locale |
string |
"en-US" |
Segment order, calendar, messages, formatting; forwarded to all children. Globally configurable. Inherited from common configuration. |
timezone |
string (IANA) |
"UTC" |
Zone used only to determine today in the calendar. Globally configurable. Inherited from common configuration. |
direction |
'ltr' | 'rtl' |
"ltr" |
Arrow-key mirroring, calendar direction, and popup alignment. Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
host |
HTMLElement |
Consumer host; its children are owned by the component. Registered with Form. |
group |
HTMLDivElement |
The labelled role=group around both fields. |
startInput |
DateInput |
Start date field (date granularity); not registered with Form. |
endInput |
DateInput |
End date field; the calendar button and popup sit inside its boundary. Not registered with Form. |
calendar |
Calendar |
The range Calendar (selectionMode ‘range’) inside the popup. |
button |
HTMLButtonElement |
Calendar button inside the end field boundary. |
popup |
HTMLDivElement |
role=dialog aria-modal=false popup (popover=manual). |
value |
Range<PlainDate> | null |
The inclusive range, or null when empty, incomplete, or failing a field or range check (a custom error from validate or setCustomError keeps the range). |
validity |
DateRangePickerValidity |
{ valid, error, message, field }; field is the field that shows the message (‘start’ | ‘end’ | null). |
isOpen |
boolean |
Whether the popup is open. |
name |
string | null |
Form field name. |
label |
string |
Group label text. |
disabled |
boolean |
Disabled state. |
dirty |
boolean |
Either date differs from its initial value. |
touched |
boolean |
Focus has left either date field at least once. |
options |
Readonly<DateRangePickerOptions> |
Current instance options. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
open(): void |
Open the calendar on the range start (else the typed start date, else today) and focus it. No-op when disabled or read-only. |
close(): void |
Close the calendar; an unfinished pick is discarded. Focus inside the popup returns to the calendar button. |
setValue(value: Range<PlainDate> | null): void |
Programmatic value (no aui-change); writes both fields and clears an external custom error. Throws when start > end. |
update(options: Partial<DateRangePickerOptions>): void |
Merge and apply options to both fields and the calendar; closes the popup when disabled or read-only. |
reportValidity(): boolean |
Show both fields’ messages now and return validity. |
setCustomError(message: string | null): void |
External error (server, cross-field rule), shown on the end field; cleared when the user changes either date. null or ‘’ clears it. |
getFormValue(): string | null |
ISO 8601 interval YYYY-MM-DD/YYYY-MM-DD, or null when there is no valid range. |
focus(): void |
Focus the first segment of the start date. |
reset(): void |
Close, restore the initial range, and clear the custom error and touched/submitted state (no aui-change). |
destroy(): void |
Close and remove rendering, listeners, registration, and subscriptions of the picker and its children. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-change |
{ value: Range<PlainDate> | null; validity: DateRangePickerValidity } |
Yes | No | User typing or a completed calendar pick changed the value or validity. Dispatched from the picker host; the inner Date Input and Calendar change events are not exposed. |
aui-navigate |
{ month: { year: number; month: number } } |
Yes | No | Bubbles from the inner Calendar when the user changes the visible months; month is the first visible month. |
aui-open |
null |
No | No | The popup opened. |
aui-close |
null |
No | No | The popup closed. |
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-date-range-picker |
Host; –disabled modifier. |
aui-date-range-picker__group |
The labelled role=group. |
aui-date-range-picker__label |
Visible group label (with the required indicator). |
aui-date-range-picker__fields |
Flex row of start field, separator, and end field; wraps when narrow. |
aui-date-range-picker__start | __end |
Hosts of the inner Date Inputs (aui-date-input classes apply inside; their labels are visually hidden). |
aui-date-range-picker__separator |
Decorative en dash between the fields (aria-hidden). |
aui-date-range-picker__description |
Help text. |
aui-date-range-picker__trailing |
Button and popup container inside the end Date Input group. |
aui-date-range-picker__button |
Calendar button. |
aui-date-range-picker__popup |
Top-layer dialog popup containing the Calendar (aui-calendar classes apply inside). |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
calendar |
calendar |
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-color-border | --aui-color-focus-ring | --aui-color-hover | --aui-color-surface | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-sm | --aui-font-weight-medium | --aui-icon-size-sm | --aui-line-height-tight | --aui-radius-md | --aui-shadow-md | --aui-space-1 | --aui-space-2 | --aui-space-3
TypeScript definitions
Section titled “TypeScript definitions”type DateRangePickerMonths = 'auto' | 1 | 2;interface DateRangePickerConfig { closeOnSelect: boolean; months: DateRangePickerMonths }interface DateRangePickerIcons { calendar: IconRef }type DateRangePickerError = DateInputError | 'incomplete-range' | 'range-reversed' | 'range-too-short' | 'range-too-long' | 'date-unavailable';interface DateRangePickerValidity { readonly valid: boolean; readonly error: DateRangePickerError | null; readonly message: string; readonly field: 'start' | 'end' | null }interface DateRangePickerChangeDetail { value: Range<PlainDate> | null; validity: DateRangePickerValidity }type Weekday = 1 | 2 | 3 | 4 | 5 | 6 | 7;Accessibility
Section titled “Accessibility”Pattern: WAI-ARIA APG Date Picker Dialog, non-modal variant, for a range: a labelled role=group containing two labelled segmented date fields (Date Input), a button with aria-haspopup=dialog inside the end field, and a role=dialog popup containing a range-selecting Calendar grid (one or two months).
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Enter / Space | Calendar button | Open (or close) the calendar; focus moves to the range start, else the typed start date, else today. |
| Alt+ArrowDown | Date segment of either field focused | Open the calendar (the segment value does not change). |
| Calendar keys | Day focused | Arrows, Home/End, PageUp/PageDown, Shift+PageUp/PageDown as in Calendar, across all visible months. |
| Enter / Space | Day focused, no pending first day | Pick the first day of the range; it is announced and the popup stays open. |
| Enter / Space | Day focused, first day pending | Pick the last day (either order), write both fields, close the popup (closeOnSelect), and focus the calendar button. |
| Escape | Popup open | Close the popup, discarding a pending first day, and return focus to the calendar button. Inside a Dialog, only the popup closes. |
| Tab / Shift+Tab | Popup open | Moves through month buttons and the day; leaving the popup (other than to the calendar button) closes it. |
| Date Input keys | Segment focused | As in Date Input; Tab moves from the start field to the end field. |
| Element | Role |
|---|---|
| range group | group, aria-labelledby the visible label |
| start and end fields | group (Date Input), each with its own visually hidden label |
| separator | none (aria-hidden=true) |
| calendar button | button (native), aria-haspopup=dialog |
| popup | dialog (aria-modal=false) |
| calendar | grid per month (Calendar, aria-multiselectable=true) |
| icons | none (aria-hidden=true) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-labelledby |
range group | Id of the visible label |
aria-describedby |
range group | Help text id when description is set |
aria-labelledby |
start / end field group | Id of the field’s visually hidden label (startLabel / endLabel, default “Start date” / “End date”) |
aria-label |
calendar button | “Choose dates”, or “Change dates, <long start date> to <long end date>” when a range is set |
aria-expanded |
calendar button | “true” while the popup is open |
aria-controls |
calendar button | Popup id |
aria-label |
popup | Localized “Choose dates” |
aria-modal |
popup | “false” (non-modal; the page stays reachable) |
aria-invalid / aria-describedby |
segments of the field that shows the error | Set by that Date Input when its message is shown |
Opening moves focus into the grid; closing with focus inside the popup returns it to the calendar button. Outside pointer presses close the popup. The button and popup sit inside the end field, so moving focus between the end segments, the button, and the popup does not mark the end field touched; leaving the start field marks the start field touched.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- The segments remain the primary input; the calendar is an alternative (APG).
- The popup is non-modal: focus is not trapped, and the rest of the page stays reachable.
- Each field has its own accessible name (visually hidden) inside the labelled group, so screen readers announce the group label and ‘Start date’ or ‘End date’.
- Range errors are shown on the field they concern (for example a reversed range on the end field) through that Date Input’s message and aria-describedby.
- The calendar names the first and last day (‘start of range’, ‘end of range’) and announces the pending first day through a polite live region.
- A typed start or end day rejected by isDateDisabled produces a localized message, so the typed and picked paths agree.
- Screen-reader behavior (group and field names, dialog announcement, range day names, the pending-day announcement) has not yet been manually verified with NVDA, JAWS, and VoiceOver.
Behavior
Section titled “Behavior”States
Section titled “States”empty, partial (one date, or an incomplete date), valid, invalid (Date Input errors, incomplete range, reversed, too short, too long, unavailable day, custom), open, open with a pending first day, disabled, read-only.
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| closed | button, or Alt+ArrowDown on a segment of either field | open; focus on the range start, the typed start date, or today; aui-open |
| open | pick a day (no pending day) | first day pending and announced; no aui-change |
| open, first day pending | pick a day | range completed (either order); both fields written; aui-change if changed; closed and focus on the button (closeOnSelect) |
| open, first day pending | Escape, pointer outside, or Tab out | closed; the pending day is discarded and the previous range stays |
| open | Escape | closed; focus on the button; aui-close |
| any | user types in either field | aui-change when the value or validity changes |
| any | reset() |
closed, initial range, pristine, untouched |
Accessibility notes
Section titled “Accessibility notes”- The segments stay the primary input; the calendar is an alternative.
- The popup is non-modal: focus is not trapped and the page stays reachable. Escape and outside presses close it through the shared overlay stack, so only the topmost overlay responds.
- Range errors are shown on the field they concern, with that Date Input’s message timing.
- A typed start or end day rejected by
isDateDisabledgets a localized message, so the typed and picked paths agree. - Manual screen-reader verification is pending.
Responsive behavior
Section titled “Responsive behavior”The start and end fields sit on one line and wrap below each other when space runs out. The popup is positioned under the fields (bottom-start), flips above when there is no room, and scrolls when taller than the available height (--aui-available-height). With months: 'auto', the popup shows two months when the viewport is at least 40rem wide when it opens, else one; the Calendar’s months wrap when the popup is narrower than two months.
Right-to-left
Section titled “Right-to-left”Segment order follows the locale; the button sits at the inline end of the end field; the popup aligns to the fields’ inline start; the calendar mirrors arrows and chevrons.
Theming
Section titled “Theming”Role tokens only; verified in the light, dark, and high-contrast themes. In forced-colors mode the popup has a CanvasText border and a disabled button uses GrayText.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ common: { timezone: 'Europe/Berlin' }, components: { dateRangePicker: { months: 2, closeOnSelect: false } } });Changes apply to existing instances immediately (months on the next open). Configuration changes that do not change locale, timezone, or direction do not rebuild the fields, so partially typed input is kept.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic.
Timezones
Section titled “Timezones”Values are PlainDate ranges and never shift. The configured timezone (default UTC, never the host zone) only determines “today” in the calendar, which is focused when the fields are empty.
icons.calendar (default calendar).
Security
Section titled “Security”All text uses textContent and Intl formatting; no HTML parsing. The popup uses the Popover API; no inline styles or handlers. CSP-safe.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. When name is set, a hidden input carries the ISO 8601 interval YYYY-MM-DD/YYYY-MM-DD (empty when there is no valid range); Form gets the same string. startName and endName give each inner Date Input’s hidden input a name (YYYY-MM-DD) for native form posts. A disabled picker is not submitted.
Performance
Section titled “Performance”Two Date Inputs and one Calendar per picker, created once; opening sets the month count and range and re-renders only the calendar. Budget: under 15 ms average per DateRangePicker.create across 100 instances.
Supported data scale
Section titled “Supported data scale”100 pickers per page.
Edge cases
Section titled “Edge cases”- Picking the last day before the first one orders the range; picking the same day twice gives a one-day range.
- With a first day pending, days that would make the range shorter than
minDaysor longer thanmaxDaysare disabled. minDaysandmaxDayscount both ends: 5 to 7 March is 3 days.maxDays: 1gives the message “Choose a single day.”- A range may include disabled days:
isDateDisabledis checked on the start and end only. - Closing with a first day pending keeps the previous range; the next open starts fresh.
- Typing a start date only gives
incomplete-range(“Enter an end date.”) on the end field, shown after the end field is touched or onreportValidity(). validatealso runs when both fields are empty (valuenull).- Opening from an empty required field does not show “This field is required.” until focus leaves the field.
- Inside a modal Dialog, the first Escape closes the popup and the second closes the dialog.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
DATE_RANGE_PICKER_OPTIONS |
configuration | Missing or empty label, empty startLabel/endLabel, value not a range of PlainDate or start > end (also from setValue), min/max not a PlainDate, min > max, minDays/maxDays not an integer 1-100000, minDays > maxDays, or a non-function isDateDisabled/validate. |
CALENDAR_OPTIONS |
configuration | firstDayOfWeek outside 1-7. |
CFG_* |
configuration | Invalid locale, timezone, direction, closeOnSelect, or months. |
Validation (validity.error) |
validation | See below. |
validity.error |
Field | Message |
|---|---|---|
Date Input errors (incomplete, invalid-date, value-missing, range-underflow, range-overflow) |
the field concerned (start first) | Date Input messages |
incomplete-range |
the empty field | “Enter a start date.” / “Enter an end date.” |
range-reversed |
end | “Enter an end date on or after the start date.” |
date-unavailable |
start or end | “This date is not available.” |
range-too-short |
end | “Choose a range of at least {count} days.” |
range-too-long |
end | “Choose a range of {count} days or fewer.” (maxDays: 1: “Choose a single day.”) |
custom |
end | validate result, then the external setCustomError message |
Precedence follows the table order. Date Input errors other than incomplete and value-missing show immediately; incomplete, required, range, and custom messages show after the field is touched or on reportValidity(). A failed construction leaves no rendering, listeners, registrations, or subscriptions behind.
Known limitations
Section titled “Known limitations”- Gregorian calendar only; years 1 to 9999.
- A range may include disabled days: only the start and end days are checked against
isDateDisabled. minDaysandmaxDayscount both the start and the end day.- Validation messages follow Date Input timing;
validationTimingis not supported. validityreports one error at a time.- Closing the popup with only the first day picked discards that pick.
- No Cancel/OK buttons: picking the last day commits.
- Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
Unsupported scenarios
Section titled “Unsupported scenarios”- Time of day in either field.
- Open-ended ranges (one date only) as a valid value.
- More than two months in the popup.
- HTML in the label, help text, or messages.