Accessibility pattern · Form inputs

Date picker

Typing the date is always allowed, with the format shown and checked; the calendar is a second way in, never the only one. The calendar is a modal dialog around a grid: one day is in the Tab order, the arrow keys move by day and week, Page Up and Page Down by month, and Enter picks the day and sends focus back.

WCAG criteria
7
Keyboard rules
9
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Mumbai CSMT to Pune Junction

Deccan Queen, chair car

Format: DD/MM/YYYY. Trains can be booked from today.

Monday Tuesday Wednesday Thursday Friday Saturday Sunday
  • Today
  • Selected
  • Unavailable

Arrow keys move by day and week. Page Up and Page Down change the month.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Enter or SpaceOn the calendar button, opens the dialog with focus on the chosen date, or today.
Arrow Left or Arrow RightIn the grid, moves to the previous or next day, crossing into the next month when needed.
Arrow Up or Arrow DownMoves to the same day of the previous or next week.
Home or EndMoves to the first (Monday) or last (Sunday) day of the week.
Page Up or Page DownShows the previous or next month, keeping the day of the month where it can.
ShiftPage Up or ShiftPage DownShows the same month in the previous or next year.
Enter or SpaceOn a day, chooses it, fills the field, closes the dialog and returns focus to the calendar button.
EscapeCloses the dialog without changing the date; focus returns to the calendar button.
Tab or ShiftTabMoves between the month and year buttons, the grid and Cancel, inside the dialog only.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches the calendar buttonChoose date, button, opens a dialog
The dialog opensChoose journey date, dialog. Arrow keys move by day and week. Monday, 5 October 2026, current date
Page Down shows the next monthNovember 2026 (from the live month name), then the focused day
Enter chooses a dayChange date, Saturday, 14 November 2026, button
A typed date cannot be readEnter the date as DD/MM/YYYY.

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-date-picker" data-ap-date-picker data-region="IN">
  <div class="ap-date-picker__trip">
    <span class="ap-date-picker__badge" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="6" y="3" width="12" height="14" rx="3"/><path d="M6 11h12M9 21l1.5-4M15 21l-1.5-4"/><path d="M9.5 14h.01M14.5 14h.01"/></svg></span>
    <div>
      <p class="ap-date-picker__route">Mumbai CSMT to Pune Junction</p>
      <p class="ap-date-picker__train">Deccan Queen, chair car</p>
    </div>
  </div>

  <div class="ap-field">
    <label class="ap-label" for="date-picker-input">Journey date</label>
    <p class="ap-hint" id="date-picker-hint">Format: DD/MM/YYYY. Trains can be booked from today.</p>
    <div class="ap-date-picker__group" data-ap-group>
      <input class="ap-input ap-date-picker__input" id="date-picker-input" type="text" autocomplete="off" spellcheck="false" aria-describedby="date-picker-hint date-picker-error" />
      <button type="button" class="ap-date-picker__open" data-ap-open aria-haspopup="dialog">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="5" width="17" height="15" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4"/><path d="M8 14h.01M12 14h.01M16 14h.01M8 17h.01M12 17h.01"/></svg>
        <span class="ap-date-picker__sr" data-ap-word>Choose date</span>
        <span class="ap-date-picker__sr" data-ap-chosen></span>
      </button>
    </div>
    <p class="ap-error ap-date-picker__error" id="date-picker-error" aria-live="polite">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.5M12 16.5h.01"/></svg>
      <span data-ap-error></span>
    </p>
  </div>

  <dialog class="ap-date-picker__dialog" aria-label="Choose journey date" aria-describedby="date-picker-keys">
    <div class="ap-date-picker__panel">
      <div class="ap-date-picker__head">
        <button type="button" class="ap-date-picker__nav" data-ap-step="-12" aria-label="Previous year"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m12 17-5-5 5-5M18 17l-5-5 5-5"/></svg></button>
        <button type="button" class="ap-date-picker__nav" data-ap-step="-1" aria-label="Previous month"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m15 18-6-6 6-6"/></svg></button>
        <h2 class="ap-date-picker__month" id="date-picker-month" aria-live="polite" data-ap-month></h2>
        <button type="button" class="ap-date-picker__nav" data-ap-step="1" aria-label="Next month"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 18 6-6-6-6"/></svg></button>
        <button type="button" class="ap-date-picker__nav" data-ap-step="12" aria-label="Next year"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m12 17 5-5-5-5M6 17l5-5-5-5"/></svg></button>
      </div>
      <table class="ap-date-picker__grid" role="grid" aria-labelledby="date-picker-month">
        <thead>
          <tr>
            <th scope="col"><span aria-hidden="true">Mo</span><span class="ap-date-picker__sr">Monday</span></th>
            <th scope="col"><span aria-hidden="true">Tu</span><span class="ap-date-picker__sr">Tuesday</span></th>
            <th scope="col"><span aria-hidden="true">We</span><span class="ap-date-picker__sr">Wednesday</span></th>
            <th scope="col"><span aria-hidden="true">Th</span><span class="ap-date-picker__sr">Thursday</span></th>
            <th scope="col"><span aria-hidden="true">Fr</span><span class="ap-date-picker__sr">Friday</span></th>
            <th scope="col"><span aria-hidden="true">Sa</span><span class="ap-date-picker__sr">Saturday</span></th>
            <th scope="col"><span aria-hidden="true">Su</span><span class="ap-date-picker__sr">Sunday</span></th>
          </tr>
        </thead>
        <tbody data-ap-days></tbody>
      </table>
      <div class="ap-date-picker__foot">
        <ul class="ap-date-picker__legend">
          <li><span class="ap-date-picker__key ap-date-picker__key--today" aria-hidden="true"></span>Today</li>
          <li><span class="ap-date-picker__key ap-date-picker__key--selected" aria-hidden="true"></span>Selected</li>
          <li><span class="ap-date-picker__key ap-date-picker__key--past" aria-hidden="true">8</span>Unavailable</li>
        </ul>
      </div>
      <div class="ap-date-picker__end">
        <p class="ap-date-picker__keys" id="date-picker-keys">Arrow keys move by day and week. Page Up and Page Down change the month.</p>
        <button type="button" class="ap-btn ap-date-picker__cancel" data-ap-cancel>Cancel</button>
      </div>
    </div>
  </dialog>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The calendar is a real table with column headers, so each day is heard with its weekday, and the grid is named by the month.

  • 1.4.1 Use of Color Level A

    Today has a ring and bold type, the chosen day a solid fill, and unavailable days are struck through; the legend says so in words.

  • 2.1.1 Keyboard Level A

    Every date can be typed, and every part of the calendar works from the keyboard.

  • 2.4.3 Focus Order Level A

    Focus moves to the chosen day when the dialog opens and back to the calendar button when it closes.

  • 3.3.1 Error Identification Level A

    A date that cannot be read, does not exist or is in the past is named in text beside the field and marks it invalid.

  • 3.3.2 Labels or Instructions Level A

    The field's label and its format hint are visible and tied to the field in code.

  • 4.1.2 Name, Role, Value Level A

    Days carry aria-selected, aria-current="date" and aria-disabled; the button's name says which date is chosen.

Usage

When to use it

Use it

  • Dates near today that people think of on a calendar, like a journey, a booking or a delivery slot.
  • When the weekday matters to the choice, such as avoiding a weekend.

Use something else

  • Dates people know by heart, such as a date of birth: use three text fields for day, month and year.
  • Ranges with a start and an end: use a range picker that keeps both visible.
  • A field where the calendar is the only way in; typing must always work.

Common failures

How it usually goes wrong

  • No way to type the date

    A read-only field that only opens a calendar makes everyone page through months, and fails people whose tools fill fields for them. The text field here takes a typed date.

  • Every day in the Tab order

    Forty-two tab stops before the Cancel button is unusable. One day is in the Tab order and the arrow keys move between days.

  • Today and the selection shown by color alone

    A tinted circle tells nothing to people who cannot see the difference. Today has a ring and bold type, the chosen day a fill, and both are in the legend and announced.

  • Day cells named only by their number

    "14" on its own does not say which month or weekday it is. Each day is named with its full date.

  • Month changes that are not announced

    Page Down silently swaps the grid. The month name is a polite live region, so the new month is heard.

  • The format kept secret

    A placeholder vanishes as soon as typing starts. The format is a hint below the label, and errors repeat it.

Notes

Building it

  • Dates are formatted with Intl.DateTimeFormat in the page's language, so the month and weekday names follow a translated page without a dictionary.
  • The dialog opens under the field when there is room and centred otherwise; showModal() keeps it modal either way.
  • Days before today are struck through and marked aria-disabled; the arrow keys stop at today, so focus never lands on a day that cannot be chosen.
  • After a choice, the calendar button's name becomes "Change date" and the date, which is what a screen reader hears as focus returns to it.
  • Six rows are always drawn, so the dialog keeps its size from month to month and the buttons do not jump.

Sources: WAI-ARIA Authoring Practices: Date Picker Dialog example · GOV.UK Design System: Date input

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction