TimePicker

A time of day or a range of them, chosen from scrolling columns or typed into the field.

Accessibility

What TimePicker does for you, and what it leaves to the page around it. The field is the accessible control, and the panel is for the pointer.

What it renders

Two parts that are not linked: a real <input> in the field, and a panel of scrolling columns rendered in a portal at the end of the document.
  • The field is a real text input. It takes an id, accepts typing, and sets aria-invalid while its text does not parse.
  • Nothing announces the panel. The input has no aria-expanded, aria-controls or aria-haspopup, and the panel has no role. A screen reader hears only that a text field has focus.
  • Inside the panel, only the OK button and your renderExtraFooter content can take focus. The Now shortcut is a link with no href: clickable, but not focusable.

Naming it

id lands on the field's <input>, so a <label htmlFor> names it. A Form.Item wires the id and the label for you.
  • TimeRangePicker needs two names. Its id takes { start, end }, one per input. One label for the whole control leaves the second field unnamed.
  • The clear button is already named from the locale's "clear" text, and it can take focus.
  • A custom suffix icon is not hidden. Add aria-hidden to your own suffixIcon, since the field already says what it is.
  • A custom range separator is read out. The default arrow is hidden from screen readers, but your own separator is not. Pass a word, or nothing.
  • Put the format where it can be read: in the placeholder, or in help text tied to the field. A reader who cannot see the panel has no other way to learn that the field wants HH:mm.

The keyboard, in the field

Everything the keyboard can do happens in the field.
  • Tab keeps the value and moves on. A reader who types a time and tabs away keeps it, so waiting for OK does not trap the keyboard.
  • Escape reverts and closes. The value goes back to what it was before the panel opened.
  • A mask format gives the best keyboard support. With format={{ format: "HH:mm", type: "mask" }}, Left and Right move between hour and minute, Up and Down step the value, Backspace clears a part, and digits fill it and move on, whether or not the panel is open.
  • Without a mask, the field is a plain text input: the reader types a time in the format, and it parses or it does not.
  • Set preserveInvalidOnBlur to keep text that does not parse instead of reverting it. Rewriting what someone typed without telling them loses their input.

The columns are pointer-only

Each column is a list of items with click and hover handlers. They have no tabIndex, no role, no aria-selected and no key handling.
  • The keyboard cannot reach them. Tab from the field skips the columns and goes to the OK button, or out of the control.
  • The selected cell is shown by color alone. A screen reader cannot tell which cell is current.
  • So inputReadOnly, a function format (which turns it on) and any display-only field leave keyboard users stuck. When a value must be picked but not typed, use a Select, which is a real listbox.
  • changeOnScroll only helps the pointer, and it can pick a value the reader did not mean.
  • A disabled cell is only gray, so write the reason in text beside the field.

What gets announced

A screen reader reads the input's value, so format decides what it says.
  • Announce the time in full. A 12-hour format without AM or PM (h:mm with no A) reads "9:30" for two times of day. Keep the A, or use the 24-hour clock.
  • Clicking a cell updates the field's value, so the picked time is read out. Anything the panel shows and the field does not is lost, which is one more reason to keep format and columns matched.
  • aria-invalid says something is wrong, not what. Pair it with a message, since status only adds a color.
  • With needConfirm on (the default), the OK button is the only control in the panel the keyboard reaches. Keep it. Enter from the field also confirms.
  • If the page repeats the chosen time elsewhere (a summary, a "closes at" label), put a polite live region there. The picker does not announce changes.

Contrast and motion

  • The selected cell is shown by its fill, so check two pairs: the cell's background against the panel, and its text against that background.
  • A disabled cell is gray text on the panel, the pair most likely to fail and the one that carries meaning. Check it in your own theme. The Tokens tab lists what this component reads.
  • The column scroll ignores reduced motion. It is animated in JavaScript, so neither prefers-reduced-motion nor the motion: false seed stops it.
  • Fewer cells is the only fix. minuteStep and showSecond={false} shorten the scroll, and defaultOpenValue starts it near where the reader is going.
  • The panel's fade does follow the motion token, so turning motion off still helps there.