Accessibility
The calendar follows the ARIA date grid pattern. Screen readers get names, states and a single tab stop per month. The keyboard page lists every key.
Grid semantics
| Part | Markup |
|---|---|
| Month | role="grid", named by the month and year, such as "October 2026" |
| Week | role="row" |
| Day | role="gridcell" with a full date as aria-label, such as "Thursday, 15 October 2026" |
| Week number | role="rowheader" labeled by labels.week and the number, such as "Week 42" |
| Day name row | aria-hidden="true", because each day already has a full date label |
| Filler rows | role="presentation" and aria-hidden="true" |
Day labels and the month name use the calendar's own names and the locale you set, always in the Gregorian calendar. Custom monthNames appear in the labels.
Day states
aria-selectedis set on days when the calendar selects something. It is absent on a display-only calendar.aria-disabled="true"marks days blocked bylimits,disabledDates,enabledDatesordisabledDayNames.- Today has
aria-current="date"and "Today" at the end of its label. The word comes fromlabels.today. - The first and last day of a date range end their label with "range start" or "range end", from
labels.rangeStartandlabels.rangeEnd. A one day range gets both. - A day in a numbered range has its range number added to the label, such as "Thursday, 15 October 2026, Range 2, range start". The word comes from
labels.range. The number badge drawn on the day isaria-hiddenand comes from adata-numberattribute, so the visible text of the day is only its number. - Days in a multiple date range carry
aria-keyshortcuts="Delete".
Roving focus
Each month has one tab stop, so Tab crosses the grid in one step. The stop sits on the day you last focused, then on the selected day, then on today, then on the first enabled day. The other days have tabindex="-1" and are reached with the arrow keys.
Focus stays on the same day when the calendar re-renders, for example after a month change. If that day leaves the screen, focus goes to the tab stop of the same month.
Month changes
A visually hidden aria-live="polite" region holds the visible months, such as "October 2026" or "October 2026, November 2026". It updates on every month change, so a screen reader announces the new month. The calendar's root element has tabindex="-1", so it takes focus from a click on plain text but is not a tab stop: the tab stops are the inputs, the controls and the grid. The dialog id is assigned after mount, so a server render has no id or aria-controls to mismatch on hydration.
Controls
| Control | Markup |
|---|---|
| Arrows | <button> with the label from labels.previousMonth or labels.nextMonth, and aria-disabled at a limit |
| Month and year titles | role="button", tabindex="0" and aria-expanded, when the pickers are on |
| Month and year picker | A role="group" named by labels.chooseMonth or labels.chooseYear. The year at its top is a <button>, aria-disabled until you page through the years, and it returns to the current years |
| Picker items | role="button" with aria-pressed on the current one |
| Presets | A role="group" named by labels.quickRanges, holding buttons with aria-pressed and aria-disabled |
| Clear button | role="button" with aria-disabled when there is nothing to clear |
| Time picker | Two role="listbox" lists named by labels.hour and labels.minute, with role="option" items and aria-selected |
| Time picker close | role="button" labeled by labels.close |
| Time picker range edges | Two role="button" toggles with aria-pressed on the edge the lists edit |
| Tags input of the multiple date picker | An input named by labels.addDate, and one <button> per date labeled labels.removeDate plus the date. Removing a date returns focus to the input |
Arrow slots with your own button or link keep their own focus and semantics.
Modal calendars
In modal mode, the calendar is a role="dialog" named by labels.calendar. Its inputs are role="combobox" with aria-haspopup="dialog", an aria-controls that points at the dialog, an aria-expanded that tracks it, and an aria-label and title from labels.date, labels.startDate or labels.endDate. Inputs you render in the datePickerInput and dateRangeInputs slots are your markup, so add these attributes yourself. Both slots pass dialogId, which is empty until the calendar mounts, so bind :aria-controls="dialogId || null".
Focus follows these rules:
- Focusing an input, or pressing ArrowDown or Alt+ArrowDown on it, opens the calendar.
- Escape closes the calendar and returns focus to the input that opened it.
- Selecting a day with the keyboard in an auto-closing calendar returns focus to the input.
- Focus leaving the calendar and its inputs closes it.
- Escape inside a month or year picker, or the time picker, closes only that layer.
- Moving focus out of an open month or year picker closes it, in modal and inline calendars. Tab from the last item goes to the titles, and the first Tab past the picker's own title closes it, so focus never lands in the grid hidden under it.
Forced colors
In Windows high contrast and other forced colors modes, the calendar uses system colors. Selected days, range ends, marked dates, the selected preset, month and year, and the selected time use Highlight and HighlightText. Days inside a range get a line above and below, today is underlined, and the range number and remove badges keep a filled shape. Your --vfc-* colors do not apply in this mode.
Right to left
Put dir="rtl" on the page or an ancestor, or set the CSS direction to rtl. The grid mirrors, and ArrowLeft and ArrowRight follow the screen: ArrowLeft moves to the day on the left. The navigation chevrons mirror under a dir="rtl" attribute, and the range bands, their rounded ends, the week number divider and the range badges use logical properties. The range shapes and badges keep a physical left and right fallback for browsers without logical properties, which render them left to right. A page that only sets direction: rtl in CSS mirrors the grid and the keys, but not the chevrons.
Focus ring
Focused controls show a 2px outline in --vfc-focus. It defaults to the primary color, so change it if your primary color has low contrast on your background.
Translate it
Every string a screen reader hears comes from labels or from locale. See locale and labels.