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-partattribute 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
idto theuseMachinehook. 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
valueproperty must be an instance ofTimeexported 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
stepsproperty 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
minandmaxproperties must be an instance ofTimeexported 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
portalledproperty 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-disabledattribute.
Methods and Properties
Machine Context
The time picker machine exposes the following context properties:
localestringThe locale (BCP 47 language tag) to use when formatting the time.valueTime | undefinedThe selected time.openbooleanWhether the timepicker is openopen.controlledbooleanWhether the datepicker open state is controlled by the useridsElementIdsThe ids of the elements in the date picker. Useful for composition.namestringThe `name` attribute of the input element.positioningPositioningOptionsThe user provided options used to position the time picker contentplaceholderstringThe placeholder text of the input.disabledbooleanWhether the time picker is disabled.readOnlybooleanWhether the time picker is read-only.minTimeThe minimum time that can be selected.maxTimeThe maximum time that can be selected.steps{ hour?: number; minute?: number; second?: number; }The steps of each time unit.withSecondsbooleanWhether to show the seconds.onValueChange(value: ValueChangeDetails) => voidFunction called when the value changes.onOpenChange(details: OpenChangeDetails) => voidFunction called when the time picker opens or closes.onFocusChange(details: FocusChangeDetails) => voidFunction called when the focused date changes.
Machine API
The time picker api exposes the following methods:
isFocusedbooleanWhether the input is focusedisOpenbooleanWhether the time picker is openvalueTime | undefinedThe selected timevalueAsStringstring | undefinedThe selected time as a stringis12HourFormatbooleanWhether the time picker is in 12-hour format (based on the locale prop)reposition(options?: PositioningOptions) => voidFunction to reposition the time picker contentopen() => voidFunction to open the time pickerclose() => voidFunction to close the time pickerclearValue() => voidFunction 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