Date Picker

How the Date Picker works with a keyboard and assistive tech, and where it stands against WCAG 2.2 AA.

AAWCAG 2.2 target
20/24contrast pairs passing
8keyboard interactions

Each month is a WAI-ARIA grid with one tab stop: arrows move by a day or a week, Page Up and Page Down by a month, and every day is announced by its full date. The panel itself is a group. When it opens from a field, it belongs in a dialog that returns focus to that field.

Keyboard#

Tab

Moves between the month buttons, the grid (on the focused day), the presets and the footer buttons.

→

Moves to the next day, across the edge of the month. ← moves to the previous day.

↓

Moves to the same day next week. ↑ moves to the same day last week.

Home

Moves to the first day of the week. End moves to the last.

Page Up

Moves to the same day in the previous month. Page Down moves to the next.

Shift+Page Up

Moves to the same day in the previous year. With Page Down, the next year.

Enter

Chooses the focused day. Space does the same. In a range, the first choice sets the start and the second the end.

Escape

In a dialog, closes the picker without saving and returns focus to the field. This belongs to the dialog, not the panel.

Semantics#

role="grid"

Labelled by the month heading. Weekday names are columnheaders with the full name (Monday) as their label.

Each month
role="gridcell"

A button with roving tabindex: only the focused day is in the tab order.

Each day
aria-label

The full date, such as Tuesday, September 29, 2026, plus has events on a marked day.

Each day
aria-selected

On the chosen day, or both ends of a range.

Chosen day
aria-current="date"

So today is announced, not only ringed in blue.

Today
aria-disabled

Days outside the allowed dates stay focusable so arrow keys can pass them, but cannot be chosen. Days from the next or previous month are hidden.

Unavailable days
aria-live="polite"

A change of month is announced, and so is the range spelled out in the footer.

Month heading, Range
aria-pressed

Each preset is a toggle button; the active one is pressed.

Presets

Colour contrast#

Every variant and state has to meet WCAG 2.2 Level AA. Measure the colours as they land on screen: a translucent hover or press wash over whatever sits behind it.

4.5:1

Day numbers · Basic, Today, Range, Active

Plain days are #6d6d6d on white; today and in-range days are #005cf8, the range on a 10% blue band; the chosen day is white on #005cf8. All pass.

WCAG 1.4.3 Contrast (Minimum) ↗
4.5:1

Weekday names · Weekday row

MON to SUN are drawn in #cbcbcb, 1.62:1 on white. They are real text, not disabled text, and fail.

WCAG 1.4.3 Contrast (Minimum) ↗
3:1

Today and event marks · Today, Marked

Today's 1px ring and the 3px event dot are #005cf8, 5.4:1 on white. Both are also in the day's name, so colour is not the only signal.

WCAG 1.4.11 Non-text Contrast ↗WCAG 1.4.1 Use of Color ↗
Not colour alone

Range band · Range

The 10% blue band is too pale to read as a boundary on its own. The start and end are filled, aria-selected, and spelled out in the footer.

WCAG 1.4.1 Use of Color ↗
3:1

Focus indicator · Focus visible

A 2px ring in #1a6ef8 off a white gap, on keyboard focus only. It measures 4.54:1 on white, so it clears 3:1 on every light surface the library uses.

WCAG 1.4.11 Non-text Contrast ↗WCAG 2.4.13 Focus Appearance ↗
Not required

Disabled days · Disabled

Days outside the month or the allowed dates are #cbcbcb and exempt as inactive.

WCAG 1.4.3 Contrast (Minimum) ↗

Measured contrast#

Text against what sits directly behind it on the white panel, from the ds.css tokens; the range band is 10% #005cf8 over white, and a hovered day turns its cell white; text is held to 4.5:1 and the arrow glyph to 3:1. 4 of 24 pairs fall below their minimum (4.5:1 for text, 3:1 for graphics) and are marked in amber.

Part · elementRestHover
Day · plain5.17passes14.35passes
Day · today5.40passes14.35passes
Day · in range4.67passes5.40passes
Day · chosen5.40passes5.40passes
Header · weekday1.62below the minimum1.62below the minimum
Header · month and year5.17passes5.17passes
Header · arrow glyph5.17passes4.75passes
Presets · label5.17passes4.62passes
Footer · Range:3.23below the minimum3.23below the minimum
Footer · Save5.40passes8.53passes
Footer · Cancel5.17passes4.62passes
Footer · Reset14.35passes13.17passes

Motion#

  • Day cells, month buttons and footer buttons change colour over 120ms. All are turned off under prefers-reduced-motion.
  • Changing month swaps the grid in place; nothing slides.

Checklist#

Open the picker from a field in a dialog: move focus to the chosen day (or today), trap Tab inside, close on Escape, and return focus to the field.

Let people type the date into the field as well. The grid is for browsing, not the only way in.

Day names come from the en-US locale. Match the page's language so they are read correctly.

The weekday row (#cbcbcb, 1.62:1) and the muted Range: label (#8f8f8f, 3.23:1) are below 4.5:1 and need darker shades before the code library ships.

Day cells are 40px and the month buttons 32px, both over the 24px minimum.

Say why a day is unavailable (fully booked, in the past) in text near the picker; a grey number alone does not explain it.