Accessibility pattern · Form inputs

Time picker

Most people want one of a few times, so the suggestions are a native radio group: one Tab stop, arrow keys to choose, and disabled times skipped with the reason written beside them. A native time field covers every other time; the script checks it against opening hours, the quarter hour and the booked slots, and a status message confirms the booking.

WCAG criteria
7
Keyboard rules
6
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.

Video call with Dr Kavya Menon

Tomorrow · 30 minutes · times in IST

Suggested times

Times that are taken say why, and cannot be chosen.

Between 09:00 and 18:00, on the quarter hour.

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
TabMoves into the suggested times (onto the chosen one, or the first free one), then to the time field and the button.
Arrow Down or Arrow RightChooses the next free time, skipping taken ones and wrapping at the end.
Arrow Up or Arrow LeftChooses the previous free time.
SpaceChooses the focused time when none is chosen yet.
Arrow Up or Arrow DownIn the time field, changes the hour, minutes or am/pm part that has focus.
EnterConfirms the booking, or explains what is missing and moves focus there.

Screen readers

What it announces

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

WhenExpected announcement
Tab reaches the suggested timesSuggested times, group. 09:00, radio button, not checked
Arrow Down from 09:0011:00, radio button, checked (10:00 is taken, so it is skipped)
Reading a taken time in browse mode10:00 Booked, radio button, unavailable
The booking is confirmedBooked for tomorrow at 11:00
Enter in the time field, on a time that clashes with a bookingThat time overlaps a booked slot. Choose another.

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

<form class="ap-time-picker" data-ap-time-picker data-region="IN" novalidate>
  <div class="ap-time-picker__intro">
    <span class="ap-time-picker__avatar" aria-hidden="true">KM</span>
    <div>
      <h3 class="ap-time-picker__who">Video call with Dr Kavya Menon</h3>
      <p class="ap-time-picker__meta">Tomorrow · 30 minutes · times in IST</p>
    </div>
  </div>

  <fieldset class="ap-time-picker__set" aria-describedby="time-picker-slot-error">
    <legend class="ap-time-picker__legend">Suggested times</legend>
    <p class="ap-hint">Times that are taken say why, and cannot be chosen.</p>
    <p class="ap-error ap-time-picker__error" id="time-picker-slot-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-slot-error></span></p>
    <div class="ap-time-picker__grid">
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="09:00" />
        <span class="ap-time-picker__time" data-ap-time="09:00">09:00</span>
        <svg class="ap-time-picker__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg>
      </label>
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="10:00" disabled data-minutes="30" />
        <span class="ap-time-picker__time" data-ap-time="10:00">10:00</span>
        <span class="ap-time-picker__why">Booked</span>
      </label>
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="11:00" />
        <span class="ap-time-picker__time" data-ap-time="11:00">11:00</span>
        <svg class="ap-time-picker__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg>
      </label>
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="12:00" />
        <span class="ap-time-picker__time" data-ap-time="12:00">12:00</span>
        <svg class="ap-time-picker__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg>
      </label>
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="13:00" disabled data-minutes="60" />
        <span class="ap-time-picker__time" data-ap-time="13:00">13:00</span>
        <span class="ap-time-picker__why">Lunch break</span>
      </label>
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="14:00" />
        <span class="ap-time-picker__time" data-ap-time="14:00">14:00</span>
        <svg class="ap-time-picker__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg>
      </label>
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="15:00" />
        <span class="ap-time-picker__time" data-ap-time="15:00">15:00</span>
        <svg class="ap-time-picker__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg>
      </label>
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="16:00" disabled data-minutes="30" />
        <span class="ap-time-picker__time" data-ap-time="16:00">16:00</span>
        <span class="ap-time-picker__why">Booked</span>
      </label>
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="17:00" />
        <span class="ap-time-picker__time" data-ap-time="17:00">17:00</span>
        <svg class="ap-time-picker__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg>
      </label>
      <label class="ap-time-picker__slot">
        <input type="radio" name="time-picker-slot" value="18:00" />
        <span class="ap-time-picker__time" data-ap-time="18:00">18:00</span>
        <svg class="ap-time-picker__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg>
      </label>
    </div>
  </fieldset>

  <div class="ap-field ap-time-picker__other">
    <label class="ap-label" for="time-picker-other">Another time</label>
    <p class="ap-hint" id="time-picker-other-hint" data-ap-hint>Between 09:00 and 18:00, on the quarter hour.</p>
    <input class="ap-input ap-time-picker__input" id="time-picker-other" type="time" min="09:00" max="18:00" step="900" aria-describedby="time-picker-other-hint time-picker-other-error" />
    <p class="ap-error ap-time-picker__error" id="time-picker-other-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-other-error></span></p>
  </div>

  <button type="submit" class="ap-btn ap-btn--primary ap-time-picker__go">Confirm time</button>
  <p class="ap-time-picker__done" role="status"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 3 3 5-6"/></svg><span data-ap-done></span></p>
</form>

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 times are a fieldset with a legend, and each reason is part of its time's label, so the grouping and the reasons are in the code.

  • 1.4.1 Use of Color Level A

    The chosen time has a tick, a thicker border and bold type; taken times are struck through and say why in words.

  • 2.1.1 Keyboard Level A

    Native radio buttons and a native time field: every part works from the keyboard without extra code.

  • 2.5.8 Target Size (Minimum) Level AA

    Each time is a whole card at least 44 pixels high, so it is easy to hit with a finger.

  • 3.3.1 Error Identification Level A

    A time outside opening hours, off the quarter hour or over a booked slot is described in text, and the field is marked invalid.

  • 3.3.2 Labels or Instructions Level A

    The legend, labels and hints say what can be chosen, before anyone gets it wrong.

  • 4.1.3 Status Messages Level AA

    The confirmation and each error are polite live messages, so pressing Enter in the field is answered even though focus stays put.

Usage

When to use it

Use it

  • Booking from a short list of times, such as appointments, deliveries or table reservations.
  • When most people take a suggested time but some need another one.

Use something else

  • Any time of day with no rules: a native time field on its own is enough.
  • Long lists of slots across many days: pick the day first, then show its times.
  • Durations, like a 90-minute timer: use number fields for hours and minutes.

Common failures

How it usually goes wrong

  • Taken times that vanish

    Removing booked times leaves gaps nobody can explain. They stay in place, disabled, with the reason written under the time.

  • A reason that only appears on hover

    A tooltip on a disabled control cannot be reached with the keyboard or by touch. The reason is plain text inside the label.

  • Times as a row of buttons

    Buttons do not say which one is chosen or how many there are. Radio buttons in a fieldset say both, and the arrow keys come free.

  • A custom clock face

    Dial pickers need dragging and precise pointing. The native time field already works with the keyboard, screen readers and every phone's own picker.

  • Errors that only say "Invalid time"

    People need to know what to change. Each message names the rule: opening hours, the quarter hour, or a clash with a booking.

  • No confirmation

    Pressing Confirm and hearing nothing leaves people unsure. A status message repeats the booked time.

Notes

Building it

  • The native time field shows 12 or 24 hours by the browser's own settings; its value is always 24-hour HH:MM, so check that, never the text on screen.
  • The 12-hour option above changes how the script writes times (with Intl.DateTimeFormat); the radio values and the rules stay in 24-hour time.
  • Disabled radio buttons are skipped by the arrow keys and by Tab, so the reason must also live outside focus: the hint above the times says that taken ones explain themselves.
  • Booked slots are read from the disabled radios and their data-minutes, so the field's clash check and the cards cannot disagree.
  • The form has novalidate: the browser's own bubbles are replaced by messages tied to each field with aria-describedby.

Sources: WAI-ARIA Authoring Practices: Radio Group · MDN: input type="time"

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