Time Picker
The time picker is used to time picker a time value, independently from a date value.
This component builds on top of the native <input type=time>
experience and
provides a more customizable and consistent user experience.
Features
- Select a time value in the menu...
- ...or type it in the input.
- Use seconds for more precision.
- Use different steps for each unit.
- Set a minimum and maximum value.
- Clear button to reset the value.
- Navigate in the menu with keyboard.
- Support for different time formats.
Installation
To use the Time Picker machine in your project, run the following command in your command line:
npm install @zag-js/time-picker @zag-js/react # or yarn add @zag-js/time-picker @zag-js/react
npm install @zag-js/time-picker @zag-js/solid # or yarn add @zag-js/time-picker @zag-js/solid
npm install @zag-js/time-picker @zag-js/vue # or yarn add @zag-js/time-picker @zag-js/vue
npm install @zag-js/time-picker @zag-js/vue # or yarn add @zag-js/time-picker @zag-js/vue
Anatomy
To set up the Time Picker correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the time picker package into your project
import * as timePicker from "@zag-js/time-picker"
The Time Picker package exports two key functions:
machine
— The state machine logic for the Time Picker widget.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll also need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the Time Picker machine in your project 🔥
import * as timePicker from "@zag-js/time-picker" import { useMachine, normalizeProps, Portal } from "@zag-js/react" import { useId } from "react" export function TimePicker() { const [state, send] = useMachine(timePicker.machine({ id: useId() }), { context: controls.context, }) const api = timePicker.connect(state, send, normalizeProps) return ( <> <div {...api.rootProps}> <div {...api.controlProps} style={{ display: "flex", gap: "10px" }}> <input {...api.inputProps} /> <button {...api.triggerProps}>🗓</button> <button {...api.clearTriggerProps}>❌</button> </div> <Portal> <div {...api.positionerProps}> <div {...api.contentProps}> <div {...api.getContentColumnProps({ type: "hour" })}> {api.getAvailableHours().map((hour) => ( <button key={hour} {...api.getHourCellProps({ hour })}> {hour} </button> ))} </div> <div {...api.getContentColumnProps({ type: "minute" })}> {api.getAvailableMinutes().map((minute) => ( <button key={minute} {...api.getMinuteCellProps({ minute })}> {minute} </button> ))} </div> <div {...api.getContentColumnProps({ type: "second" })}> {api.getAvailableSeconds().map((second) => ( <button key={second} {...api.getSecondCellProps({ second })}> {second} </button> ))} </div> <div {...api.getContentColumnProps({ type: "period" })}> <button {...api.getPeriodCellProps({ period: "am" })}> AM </button> <button {...api.getPeriodCellProps({ period: "pm" })}> PM </button> </div> </div> </div> </Portal> </div> </> ) }
import * as timePicker from "@zag-js/time-picker" import { normalizeProps, useMachine } from "@zag-js/solid" import { For, createMemo, createUniqueId, type ParentProps } from "solid-js" import { Portal } from "solid-js/web" function Wrapper(props: ParentProps) { return <Portal mount={document.body}>{props.children}</Portal> } export function TimePicker() { const [state, send] = useMachine( timePicker.machine({ id: createUniqueId() }), { context: controls.context, }, ) const api = createMemo(() => timePicker.connect(state, send, normalizeProps)) return ( <> <div {...api().rootProps}> <div {...api().controlProps} style={{ display: "flex", gap: "10px" }}> <input {...api().inputProps} /> <button {...api().triggerProps}>🗓</button> <button {...api().clearTriggerProps}>❌</button> </div> <Wrapper> <div {...api().positionerProps}> <div {...api().contentProps}> <div {...api().getContentColumnProps({ type: "hour" })}> <For each={api().getAvailableHours()}> {(hour) => ( <button {...api().getHourCellProps({ hour })}> {hour} </button> )} </For> </div> <div {...api().getContentColumnProps({ type: "minute" })}> <For each={api().getAvailableMinutes()}> {(minute) => ( <button {...api().getMinuteCellProps({ minute })}> {minute} </button> )} </For> </div> <div {...api().getContentColumnProps({ type: "second" })}> <For each={api().getAvailableSeconds()}> {(second) => ( <button {...api().getSecondCellProps({ second })}> {second} </button> )} </For> </div> <div {...api().getContentColumnProps({ type: "period" })}> <button {...api().getPeriodCellProps({ period: "am" })}> AM </button> <button {...api().getPeriodCellProps({ period: "pm" })}> PM </button> </div> </div> </div> </Wrapper> </div> </> ) }
import * as timePicker from "@zag-js/time-picker" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, Teleport } from "vue" export default defineComponent({ name: "TimePicker", setup() { const [state, send] = useMachine(timePicker.machine({ id: "1" }), { context: controls.context, }) const apiRef = computed(() => timePicker.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.rootProps}> <div {...api.controlProps} style={{ display: "flex", gap: "10px" }}> <input {...api.inputProps} /> <button {...api.triggerProps}>🗓</button> <button {...api.clearTriggerProps}>❌</button> </div> <Teleport to="body"> <div {...api.positionerProps}> <div {...api.contentProps}> <div {...api.getContentColumnProps({ type: "hour" })}> {api.getAvailableHours().map((hour) => ( <button key={hour} {...api.getHourCellProps({ hour })}> {hour} </button> ))} </div> <div {...api.getContentColumnProps({ type: "minute" })}> {api.getAvailableMinutes().map((minute) => ( <button key={minute} {...api.getMinuteCellProps({ minute })} > {minute} </button> ))} </div> <div {...api.getContentColumnProps({ type: "second" })}> {api.getAvailableSeconds().map((second) => ( <button key={second} {...api.getSecondCellProps({ second })} > {second} </button> ))} </div> <div {...api.getContentColumnProps({ type: "period" })}> <button {...api.getPeriodCellProps({ period: "am" })}> AM </button> <button {...api.getPeriodCellProps({ period: "pm" })}> PM </button> </div> </div> </div> </Teleport> </div> ) } }, })
<script setup> import * as timePicker from "@zag-js/time-picker" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, Teleport } from "vue" const [state, send] = useMachine(timePicker.machine({ id: "1" })) const api = computed(() => timePicker.connect(state.value, send, normalizeProps)) </script> <template> <div v-bind="api.rootProps"> <div v-bind="api.controlProps" :style="{ display: 'flex', gap: '10px' }"> <input v-bind="api.inputProps" /> <button v-bind="api.triggerProps">🗓</button> <button v-bind="api.clearTriggerProps">❌</button> </div> <Teleport to="body"> <div v-bind="api.positionerProps"> <div v-bind="api.contentProps"> <div v-bind="api.getContentColumnProps({ type: 'hour' })"> <button v-for="hour in api.getAvailableHours()" v-bind="api.getHourCellProps({ hour })"> {{ hour }} </button> </div> <div v-bind="api.getContentColumnProps({ type: 'minute' })"> <button v-for="minute in api.getAvailableMinutes()" v-bind="api.getMinuteCellProps({ minute })"> {{ minute }} </button> </div> <div v-bind="api.getContentColumnProps({ type: 'second' })"> <button v-for="second in api.getAvailableSeconds()" v-bind="api.getSecondCellProps({ second })"> {{ second }} </button> </div> <div v-bind="api.getContentColumnProps({ type: 'period' })"> <button v-bind="api.getPeriodCellProps({ period: 'am' })">AM</button> <button v-bind="api.getPeriodCellProps({ period: 'pm' })">PM</button> </div> </div> </div> </Teleport> </template>
Setting the initial value
To set the initial value of the time picker, pass the value
property to the
time picker machine's context.
The
value
property must be an instance ofTime
exported from@internationalized/date
, or undefined.
const [state, send] = useMachine( timePicker.machine({ id: useId(), value: new Time(12, 30), }), )
Disabling the time picker
To disable the time picker, set the disabled
property in the machine's context
to true
.
const [state, send] = useMachine( timePicker.machine({ id: useId(), disabled: true, }), )
Usage with seconds
By default, the time picker only shows the hour and minute cells. To show the
second cell, set the showSeconds
property in the machine's context to true
.
const [state, send] = useMachine( timePicker.machine({ id: useId(), withSeconds: true, }), )
Setting the locale
To set the locale of the time picker, pass the locale
property to the time
picker machine's context.
This will affect the presence of the period cell and the time format.
const [state, send] = useMachine( timePicker.machine({ id: useId(), locale: "fr-FR", }), )
Setting steps
To set the steps for the time picker, pass the steps
property to the time
picker machine's context.
The
steps
property must be an object with the following properties:
hour
— The step for the hour cell.minute
— The step for the minute cell.second
— The step for the second cell.
const [state, send] = useMachine( timePicker.machine({ id: useId(), steps: { hour: 2, minute: 15, second: 30, }, }), )
Setting min and max values
To set the minimum and maximum values for the time picker, pass the min
and
max
properties to the time picker machine's context.
The
min
andmax
properties must be an instance ofTime
exported from@internationalized/date
.
const [state, send] = useMachine( timePicker.machine({ id: useId(), min: new Time(10), // Only allow times after 10:00:00 max: new Time(18, 30, 20), // Only allow times before 18:30:20 }), )
Listening for focus changes
When an item is focused with the keyboard, use the onFocusChange
to listen for
the change and do something with it.
const [state, send] = useMachine( timePicker.machine({ id: useId(), onFocusChange(details) { // details => { focusedCell: { value: number, unit: TimeUnit } } console.log(details) }, }), )
Listening for value changes
When the value changes, use the onValueChange
property to listen for the
change and do something with it.
const [state, send] = useMachine( timePicker.machine({ id: useId(), onValueChange(details) { // details => { value?: Time, valueAsString?: string } console.log(details) }, }), )
Listening for open and close events
When the time picker is opened or closed, the onOpenChange
callback is called.
You can listen for these events and do something with it.
const [state, send] = useMachine( timePicker.machine({ id: useId(), onOpenChange(details) { // details => { open: boolean } console.log("time picker opened") }, }), )
Usage within dialog
When using the time picker within a dialog, you'll need to avoid rendering the
time picker in a Portal
or Teleport
. This is because the dialog will trap
focus within it, and the time picker will be rendered outside the dialog.
Consider designing a
portalled
property in your component to allow you decide where to render the time picker in a portal.
Styling guide
Earlier, we mentioned that each time picker part has a data-part
attribute
added to them to time picker and style them in the DOM.
Open and closed state
When the time picker is open, the trigger and content is given a data-state
attribute.
[data-part="trigger"][data-state="open|closed"] { /* styles for open or closed state */ } [data-part="content"][data-state="open|closed"] { /* styles for open or closed state */ }
Cell state
Items are given a data-state
attribute, indicating whether they are selected.
[data-part="hour|minute|second|period-cell"][data-selected] { /* styles for selected or unselected state */ }
Disabled state
When the time picker is disabled, the trigger and label is given a
data-disabled
attribute.
[data-part="trigger"][data-disabled] { /* styles for disabled time picker state */ } [data-part="label"][data-disabled] { /* styles for disabled label state */ }
Optionally, when an item is disabled, it is given a
data-disabled
attribute.
Methods and Properties
Machine Context
The time picker machine exposes the following context properties:
locale
string
The locale (BCP 47 language tag) to use when formatting the time.value
Time | undefined
The selected time.open
boolean
Whether the timepicker is openopen.controlled
boolean
Whether the datepicker open state is controlled by the userids
ElementIds
The ids of the elements in the date picker. Useful for composition.name
string
The `name` attribute of the input element.positioning
PositioningOptions
The user provided options used to position the time picker contentplaceholder
string
The placeholder text of the input.disabled
boolean
Whether the time picker is disabled.readOnly
boolean
Whether the time picker is read-only.min
Time
The minimum time that can be selected.max
Time
The maximum time that can be selected.steps
{ hour?: number; minute?: number; second?: number; }
The steps of each time unit.withSeconds
boolean
Whether to show the seconds.onValueChange
(value: ValueChangeDetails) => void
Function called when the value changes.onOpenChange
(details: OpenChangeDetails) => void
Function called when the time picker opens or closes.onFocusChange
(details: FocusChangeDetails) => void
Function called when the focused date changes.
Machine API
The time picker api
exposes the following methods:
isFocused
boolean
Whether the input is focusedisOpen
boolean
Whether the time picker is openvalue
Time | undefined
The selected timevalueAsString
string | undefined
The selected time as a stringis12HourFormat
boolean
Whether the time picker is in 12-hour format (based on the locale prop)reposition
(options?: PositioningOptions) => void
Function to reposition the time picker contentopen
() => void
Function to open the time pickerclose
() => void
Function to close the time pickerclearValue
() => void
Function to clear the selected timegetAvailableHours
() => string[]
Get the available hours that will be displayed in the time pickergetAvailableMinutes
() => string[]
Get the available minutes that will be displayed in the time pickergetAvailableSeconds
() => string[]
Get the available seconds that will be displayed in the time picker
Accessibility
Adheres to the ListBox WAI-ARIA design pattern.
Keyboard Interactions
- ArrowLeftMoves focus to the previous column.
- ArrowRightMoves focus to the next column.
- ArrowUpMoves focus to the previous cell within the current column.
- ArrowDownMoves focus to the next cell within the current column.
- EnterSelects the focused hour/minute/second/period and moves focus to the next column.
- EscCloses the time picker without selecting any time.
Edit this page on GitHub