Skip to main content

Lesson 6: Time Picker

Using useTimePicker and useDateTime for time input.

The useTimePicker hook

useTimePicker handles time input — spinbutton fields, keyboard increment/decrement, and AM/PM.

import { useTimePicker } from '@gentleduck/calendar'
 
function MyTimePicker() {
  const { state, getFieldProps } = useTimePicker({
    hourCycle: '12',
    showSeconds: true,
  })
 
  return (
    <div>
      <input {...getFieldProps('hour')} />
      <span>:</span>
      <input {...getFieldProps('minute')} />
      <span>:</span>
      <input {...getFieldProps('second')} />
      <button {...getFieldProps('ampm')}>{state.displayAmPm}</button>
    </div>
  )
}
import { useTimePicker } from '@gentleduck/calendar'
 
function MyTimePicker() {
  const { state, getFieldProps } = useTimePicker({
    hourCycle: '12',
    showSeconds: true,
  })
 
  return (
    <div>
      <input {...getFieldProps('hour')} />
      <span>:</span>
      <input {...getFieldProps('minute')} />
      <span>:</span>
      <input {...getFieldProps('second')} />
      <button {...getFieldProps('ampm')}>{state.displayAmPm}</button>
    </div>
  )
}

How it works

Each field is a spinbutton. Users can:

  • Type a number to set the value.
  • Press ArrowUp / ArrowDown to increment/decrement.
  • Press Tab to move between fields.
  • On the AM/PM field: A for AM, P for PM.

getFieldProps applies:

  • role="spinbutton" with aria-valuemin, aria-valuemax, aria-valuenow.
  • Keyboard handlers for increment, decrement, and digit entry.
  • Focus management via tabIndex.

Hour cycles

CycleRangeAM/PM?
'24'0-23No
'12'1-12Yes
// 24-hour
useTimePicker({ hourCycle: '24' })
// state.value.hour is 0-23
 
// 12-hour
useTimePicker({ hourCycle: '12' })
// state.displayHour is 1-12
// state.displayAmPm is 'AM' or 'PM'
// 24-hour
useTimePicker({ hourCycle: '24' })
// state.value.hour is 0-23
 
// 12-hour
useTimePicker({ hourCycle: '12' })
// state.displayHour is 1-12
// state.displayAmPm is 'AM' or 'PM'

Combining date and time

useDateTime composes useCalendar and useTimePicker into a single hook:

import { NativeAdapter, useDateTime } from '@gentleduck/calendar'
 
const adapter = new NativeAdapter()
 
function MyDateTimePicker() {
  const { calendar, timePicker, state } = useDateTime({
    adapter,
    defaultValue: new Date(),
    hourCycle: '12',
  })
 
  return (
    <div>
      {/* Calendar UI using calendar.state, calendar.getDayProps, etc. */}
      {/* Time UI using timePicker.state, timePicker.getFieldProps, etc. */}
      <p>Combined value: {state.value?.toISOString()}</p>
    </div>
  )
}
import { NativeAdapter, useDateTime } from '@gentleduck/calendar'
 
const adapter = new NativeAdapter()
 
function MyDateTimePicker() {
  const { calendar, timePicker, state } = useDateTime({
    adapter,
    defaultValue: new Date(),
    hourCycle: '12',
  })
 
  return (
    <div>
      {/* Calendar UI using calendar.state, calendar.getDayProps, etc. */}
      {/* Time UI using timePicker.state, timePicker.getFieldProps, etc. */}
      <p>Combined value: {state.value?.toISOString()}</p>
    </div>
  )
}

state.value is a single Date that joins the selected calendar date with the current time. Date changes keep the time; time changes keep the date.


Constraints

Set minimum and maximum times:

useTimePicker({
  hourCycle: '24',
  minTime: { hour: 9, minute: 0 },
  maxTime: { hour: 17, minute: 0 },
  minuteStep: 15, // 15-minute increments
  secondStep: 5,  // 5-second increments (when showSeconds is true)
})
useTimePicker({
  hourCycle: '24',
  minTime: { hour: 9, minute: 0 },
  maxTime: { hour: 17, minute: 0 },
  minuteStep: 15, // 15-minute increments
  secondStep: 5,  // 5-second increments (when showSeconds is true)
})