TimePicker
An input that picks a time from a list, or takes a typed time. To pick a date and a time together, use DateTimePicker.
<script setup lang="ts">
import { ref } from 'vue'
import { TimePicker } from 'frappe-ui'
// "When should we sync?" — typical meeting scheduler time slot
const meetingTime = ref('14:30')
</script>
<template>
<div class="flex w-72 flex-col gap-1.5">
<span class="text-sm text-ink-gray-7">Meeting time</span>
<TimePicker
v-model="meetingTime"
placeholder="Pick a time"
side="bottom"
align="start"
>
<template #prefix>
<span class="lucide-clock size-4 text-ink-gray-5" />
</template>
</TimePicker>
<span class="text-xs text-ink-gray-5">
Selected: {{ meetingTime || '—' }}
</span>
</div>
</template>Examples
Nightly backup time
:interval="30" lists a time every 30 minutes.
<script setup lang="ts">
import { ref } from 'vue'
import { TimePicker } from 'frappe-ui'
// Schedule a nightly cron — 24h format keeps the ops team unambiguous
const cronTime = ref('02:00')
</script>
<template>
<div class="flex w-72 flex-col gap-1.5">
<span class="text-sm text-ink-gray-7">Nightly backup runs at</span>
<TimePicker
v-model="cronTime"
format="HH:mm"
:interval="30"
>
<template #prefix>
<span class="lucide-server-cog size-4 text-ink-gray-5" />
</template>
</TimePicker>
<span class="text-xs text-ink-gray-5">UTC, every day at {{ cronTime }}</span>
</div>
</template>Class schedule
options replaces the generated list with a fixed set of times, each with its own label.
<script setup lang="ts">
import { ref } from 'vue'
import { TimePicker } from 'frappe-ui'
// A yoga studio offers only its scheduled class times — not free-form intervals
const classTime = ref('07:00')
const classOptions = [
{ value: '06:00', label: '6:00 AM — Sunrise Flow' },
{ value: '07:00', label: '7:00 AM — Vinyasa' },
{ value: '09:30', label: '9:30 AM — Gentle Hatha' },
{ value: '12:15', label: '12:15 PM — Lunch Power' },
{ value: '17:30', label: '5:30 PM — Restorative' },
{ value: '19:00', label: '7:00 PM — Candlelight Yin' },
]
</script>
<template>
<div class="flex w-80 flex-col gap-1.5">
<span class="text-sm text-ink-gray-7">Book a class</span>
<TimePicker v-model="classTime" :options="classOptions">
<template #prefix>
<span class="lucide-dumbbell size-4 text-ink-gray-5" />
</template>
</TimePicker>
</div>
</template>Shift hours
min and max limit each picker. The end time's min is the start time, so a shift cannot end before it starts.
<script setup lang="ts">
import { ref } from 'vue'
import { TimePicker } from 'frappe-ui'
// Shift scheduling — limit each picker to business hours.
const shiftStart = ref('09:00')
const shiftEnd = ref('17:00')
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-3">
<span class="text-sm-medium text-ink-gray-7">Shift hours</span>
<div class="grid grid-cols-2 gap-3">
<div class="flex flex-col gap-1.5">
<span class="text-xs text-ink-gray-6">From</span>
<TimePicker
v-model="shiftStart"
min="06:00"
:max="shiftEnd"
:interval="15"
>
<template #prefix>
<span class="lucide-sunrise size-4 text-ink-gray-5" />
</template>
</TimePicker>
</div>
<div class="flex flex-col gap-1.5">
<span class="text-xs text-ink-gray-6">To</span>
<TimePicker
v-model="shiftEnd"
:min="shiftStart"
max="22:00"
:interval="15"
>
<template #prefix>
<span class="lucide-sunset size-4 text-ink-gray-5" />
</template>
</TimePicker>
</div>
</div>
<span class="text-xs text-ink-gray-5">
Working {{ shiftStart }} – {{ shiftEnd }}
</span>
</div>
</template>Behavior
Value
The value is a 24-hour 'HH:mm' string, or 'HH:mm:ss' when seconds were typed. min and max use the same form.
Display format
format is a Dayjs format string for the text in the input. The default is HH:mm. Use format="h:mm A" for a 12-hour clock. The value stays in 24-hour form either way.
Options
The list has a time every interval minutes, 15 by default. options takes { value, label? } items and replaces the generated list.
Typing a time
People can type a time into the input by default. Set :typeable="false" to allow picking from the list only. Typed text that cannot be read as a time goes back to the last valid value.
Suffix slot
#suffix replaces the chevron. It receives { open, disabled, setOpen, close }. Use setOpen(!open) for a custom chevron. close() is the same as setOpen(false).
Label, description and error
label renders above the field and description below it. error renders below the field and hides description. It takes a string, an array of strings (one line each), or an Error, the same values as ErrorMessage. An empty string or an empty array means no error. required adds a red asterisk to the label and sets required on the <input>.
The #label slot replaces the label text and the required marker, and receives { required }. A #description slot is not hidden by error. It renders above the error.
Accessibility
The input has role="combobox", aria-haspopup="listbox" and aria-expanded, so a screen reader announces that the field opens a list and whether the list is open.
API Reference
Show types
/**
* TimePicker is a thin wrapper around Combobox that adds time-aware parsing,
* generated options, and configurable display formatting. Canonical value is
* always 24-hour `HH:mm` (or `HH:mm:ss` if seconds were typed).
*/
import type { InputLabelingProps } from '../../composables/useInputLabeling'
import type { InputSize, InputVariant } from '../../composables/inputTypes'
export type PopoverSide = 'top' | 'right' | 'bottom' | 'left'
export type PopoverAlign = 'start' | 'center' | 'end'
/**
* The shared input variants, under the picker's own name. It is an alias, not
* a copy: a TimePicker-only scale would need a TimePicker-only renderer.
*/
export type Variant = InputVariant
export interface TimePickerProps extends InputLabelingProps {
/** Controlled value, canonical `HH:mm` (or `HH:mm:ss`). */
modelValue?: string
/** Minute interval between generated options. */
interval?: number
/** Caller-provided option values; bypasses the generated grid. */
options?: Array<{ value: string; label?: string }>
/** Preferred popover side. */
side?: PopoverSide
/** Alignment of the popover along the trigger edge. */
align?: PopoverAlign
/** Gap between trigger and popover in pixels. */
offset?: number
/** Placeholder text when no value is selected. */
placeholder?: string
/** Visual style variant. */
variant?: Variant
/** Visual size of the trigger input. Forwarded to the underlying `TextInput`. */
size?: InputSize
/**
* Whether the trigger input accepts typed input. When `false` the user can
* still open the popover and pick a time, but cannot type a time manually.
* Default: `true`.
*/
typeable?: boolean
/** Keeps the popover open after a time is selected. */
keepOpen?: boolean
/** Dayjs format string used for display. Default: `HH:mm`. */
format?: string
/** Disable the time picker. */
disabled?: boolean
/** Controlled popover open state. Use with `v-model:open` for two-way binding. */
open?: boolean
/** Opens the popover when the input receives focus. Default: false. */
openOnFocus?: boolean
/** Opens the popover when the input is clicked. Default: true. */
openOnClick?: boolean
/** Minimum selectable time as `HH:mm[:ss]`. */
min?: string
/** Maximum selectable time as `HH:mm[:ss]`. */
max?: string
}
/**
* `open` and `close` are gone: `update:open` carries the same two events with
* the state in the payload. `input-invalid` and `invalid-change` are gone too
* — typed text that does not parse reverts to the last valid value, which the
* user sees, and nothing listened to either event (INP-Q3).
*/
export type TimePickerEmits = {
(e: 'update:modelValue', value: string): void
(e: 'update:open', value: boolean): void
(e: 'change', value: string): void
}Controlled value, canonical `HH:mm` (or `HH:mm:ss`).
Minute interval between generated options.
Caller-provided option values; bypasses the generated grid.
Preferred popover side.
Alignment of the popover along the trigger edge.
Gap between trigger and popover in pixels.
Placeholder text when no value is selected.
Visual style variant.
Visual size of the trigger input. Forwarded to the underlying `TextInput`.
Whether the trigger input accepts typed input. When `false` the user can still open the popover and pick a time, but cannot type a time manually. Default: `true`.
Keeps the popover open after a time is selected.
Dayjs format string used for display. Default: `HH:mm`.
Disable the time picker.
Controlled popover open state. Use with `v-model:open` for two-way binding.
Opens the popover when the input receives focus. Default: false.
Opens the popover when the input is clicked. Default: true.
Minimum selectable time as `HH:mm[:ss]`.
Maximum selectable time as `HH:mm[:ss]`.
Label rendered above (or beside, for binary controls) the input.
Helper text rendered below the input. Hidden when `error` is set. A `#description` slot is not: it renders beside the error, and is referenced alongside it.
Error message rendered below the input. When set, the control receives `aria-invalid="true"` and `data-state="invalid"`. Takes a string, an array of strings, or an `Error` whose `messages` are rendered as stacked lines (with `Error.message` as the fallback). This is the same value `ErrorMessage.message` takes. An empty array and an empty string both mean no error.
Marks the field as required. Renders an asterisk next to the label, with `sr-only` text that announces it, and forwards `required` / `aria-required` to the underlying control where the control's role allows it. `data-required` is set either way.
HTML id of the underlying control. Auto-generated via `useId()` if omitted.
| Slot | Payload |
|---|---|
label | { required: boolean; } Overrides the rendered label content. Receives `{ required }`. |
description | — Overrides the rendered description content. |
prefix | — Rendered inside the trigger input, before the typed value. |
suffix | { open: boolean; disabled: boolean; setOpen: (value: boolean) => void; close: () => void; } Rendered inside the trigger input, after the typed value. Defaults to a chevron-down that toggles the popover. |
Overrides the rendered label content. Receives `{ required }`.
Overrides the rendered description content.
Rendered inside the trigger input, before the typed value.
Rendered inside the trigger input, after the typed value. Defaults to a chevron-down that toggles the popover.
| Event | Payload |
|---|---|
update:open | [value: boolean] Fired when the open state changes. |
change | [value: string] Fired after the value is committed. |
update:modelValue | [value: string] Fired when the model value changes. |
Fired when the open state changes.
Fired after the value is committed.
Fired when the model value changes.
| Template ref | Type |
|---|---|
open | () => void Opens the panel. A disabled picker stays closed. |
close | () => void Closes the panel. |
focus | (options?: FocusOptions) => void Moves focus to the input. |
Opens the panel. A disabled picker stays closed.
Closes the panel.
Moves focus to the input.