TimePicker

A time selection component combining a free-text input with a dropdown of scrollable hour, minute, optional second, and optional AM/PM columns

Installation

pnpm add @wandercom/design-system-web

Usage

5 lines
import { TimePicker } from '@wandercom/design-system-web/ui/time-picker';

export function Example() {
  return <TimePicker defaultValue="09:30 AM" interval={15} />;
}

Format tokens

The format prop accepts a colon-delimited string built from the following tokens:

  • hh — hour, two digits. Behaves as 12-hour when an a token is present, 24-hour otherwise.
  • mm — minute, two digits. Stepped by interval.
  • ss — second, two digits. Stepped by secondInterval.
  • a — AM/PM meridiem.

Supported formats: "hh:mm:a" (default), "hh:mm:ss:a", "hh:mm:ss", "hh:mm".

The value and defaultValue props are strings matching this format (e.g. "09:30 AM" for hh:mm:a, "14:45:30" for hh:mm:ss).

Examples

The default example shows the empty state, a populated value, the disabled state, and the invalid state via aria-invalid.

Loading example...
4 lines
<TimePicker />
<TimePicker defaultValue="09:30 AM" />
<TimePicker defaultValue="09:30 AM" disabled />
<TimePicker defaultValue="09:30 AM" aria-invalid />

With seconds

Add a ss token to the format to render a third scrollable column. Use secondInterval to control its step independently of interval.

Loading example...
11 lines
<TimePicker
  defaultValue="09:30:00 AM"
  format="hh:mm:ss:a"
  secondInterval={1}
/>

<TimePicker
  defaultValue="14:45:30"
  format="hh:mm:ss"
  secondInterval={5}
/>

24-hour clock

Omit the a token to render a 24-hour clock. The hour column lists 00 through 23 and the input accepts and emits 24-hour values.

Loading example...
1 lines
<TimePicker defaultValue="14:30" format="hh:mm" />

Sizes

The size prop matches the size variants on InputGroup.

Loading example...
2 lines
<TimePicker defaultValue="09:30 AM" />
<TimePicker defaultValue="09:30 AM" size="sm" />

Custom interval

The interval prop controls how the minute column is stepped. The minute portion of any committed value is snapped down to the nearest multiple.

Loading example...
3 lines
<TimePicker defaultValue="09:00 AM" interval={15} />
<TimePicker defaultValue="13:30" format="hh:mm" interval={30} />
<TimePicker defaultValue="09:07 AM" interval={1} />

Controlled

Pass value and onChange to control the time externally. The callback receives a string matching format.

Loading example...
3 lines
const [value, setValue] = useState('09:30 AM');

<TimePicker value={value} onChange={setValue} interval={5} />;

Props

TimePicker

value?:

string
Controlled time value as a string matching `format`.

defaultValue?:

string
Default time value when uncontrolled. Must match `format`.

onChange?:

(value: string) => void
Called with the new value whenever a committed selection or typed entry changes the time.

format?:

'hh:mm:a' | 'hh:mm:ss:a' | 'hh:mm:ss' | 'hh:mm'
Format string controlling which columns appear and how the value is parsed. Defaults to 'hh:mm:a'.

interval?:

number
Minute column step in minutes. Committed minute values are snapped down to the nearest multiple. Defaults to 5.

secondInterval?:

number
Second column step in seconds. Only meaningful when `format` includes `ss`. Defaults to 5.

placeholder?:

string
Placeholder displayed in the input when no value is set. Defaults to a token-shaped placeholder derived from `format` (e.g. `--:-- AM`).

open?:

boolean
Controlled open state of the dropdown.

defaultOpen?:

boolean
Default open state when uncontrolled.

onOpenChange?:

(open: boolean) => void
Called when the open state changes.

disabled?:

boolean
Disables both the input and the trigger button.

aria-invalid?:

boolean
Marks the input as invalid, applying the destructive border style.

name?:

string
Name attribute on the underlying input for form submission.

required?:

boolean
Marks the underlying input as required.

size?:

'default' | 'sm'
Visual size of the input. Mirrors the `size` prop on `InputGroup` (`default` is `h-12 text-body-lg`; `sm` is `h-10 text-body`). Defaults to 'default'.

openLabel?:

string
Accessible label for the button that opens the time picker dropdown. Defaults to "Open time picker".

pickerLabel?:

string
Accessible label for the time picker dropdown dialog. Defaults to "Time picker".

className?:

string
Additional CSS classes applied to the wrapping element.

Accessibility

  • The trigger is a button with aria-haspopup="dialog" and aria-expanded reflecting the open state. Enter and Space open the dropdown.
  • Each column inside the dropdown is a role="listbox" (labeled Hours, Minutes, Seconds, AM or PM) with role="option" cells; the active cell carries aria-selected="true".
  • On open, focus lands on the currently selected hour, or on 12 when no value is set.
  • Within a column: ArrowUp / ArrowDown move focus, Home / End jump to the first or last cell, Enter or Space selects the cell.
  • Tab and Shift+Tab move between columns in display order. Enter or Space on a cell in the last digit column commits the selection and closes the popover. The AM/PM column never auto-closes — toggling meridiem leaves the dropdown open so the user can keep adjusting.
  • Escape closes the popover without committing pending typed-but-uncommitted input changes; focus returns to the trigger.
  • Manual typing into the input is validated against format on blur and on Enter. Invalid input reverts to the last committed value; valid input is snapped to interval and secondInterval before commit.

Edge cases

  • interval and secondInterval are floored and clamped to a minimum of 1.
  • 12-hour formats expect hours 112; 24-hour formats expect 023. Values outside these ranges are treated as invalid input and revert on commit.
  • The a meridiem token matches mainstream date libraries (date-fns, dayjs, moment).
TimePicker