TimePicker
Lets users select a specific time from a list or enter a custom value. Supports 12/24-hour formats, custom intervals, and optional time ranges.
Basic
<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>24 Hour Format
<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>Custom Options
<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>Min / Max Range
<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>Labeling
<script setup lang="ts">
import { TimePicker } from 'frappe-ui'
</script>
<template>
<div class="flex w-72 flex-col gap-4 items-start">
<TimePicker label="Default" model-value="14:30" />
<TimePicker
label="With description"
description="Pick when the meeting should start."
model-value="14:30"
/>
<TimePicker label="Required" required model-value="14:30" />
<TimePicker label="Disabled" disabled model-value="14:30" />
<TimePicker
label="With error"
error="Please select a time."
model-value="14:30"
/>
</div>
</template>Sizes & Variants
<script setup lang="ts">
import { ref } from 'vue'
import { TimePicker } from 'frappe-ui'
const value = ref('14:30')
</script>
<template>
<div class="flex w-72 flex-col gap-6 items-start">
<div class="flex w-full flex-col gap-4 items-start">
<span class="text-sm text-ink-gray-7">Sizes</span>
<TimePicker v-model="value" size="sm" />
<TimePicker v-model="value" size="md" />
<TimePicker v-model="value" size="lg" />
<TimePicker v-model="value" size="xl" />
</div>
<div class="flex w-full flex-col gap-4 items-start">
<span class="text-sm text-ink-gray-7">Variants</span>
<TimePicker v-model="value" variant="subtle" />
<TimePicker v-model="value" variant="outline" />
</div>
</div>
</template>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 } from '../../composables/inputTypes'
export type PopoverSide = 'top' | 'right' | 'bottom' | 'left'
export type PopoverAlign = 'start' | 'center' | 'end'
export type Variant = 'outline' | 'subtle'
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
}
export type TimePickerEmits = {
(e: 'update:modelValue', value: string): void
(e: 'update:open', value: boolean): void
(e: 'change', value: string): void
(e: 'input-invalid', input: string): void
(e: 'invalid-change', invalid: boolean): void
(e: 'open'): void
(e: 'close'): 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"`. May be either a string or an `Error` object whose `messages?: string[]` is rendered as stacked lines (with `Error.message` as the fallback).
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 | { toggle: (flag?: boolean | Event | undefined) => void; open: boolean; } 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 |
|---|---|
open | [] Fired when the component opens. |
update:open | [value: boolean] Fired when the open state changes. |
change | [value: string] Fired after the value is committed. |
close | [] Fired when the component closes. |
update:modelValue | [value: string] Fired when the model value changes. |
input-invalid | [input: string] |
invalid-change | [invalid: boolean] |
Fired when the component opens.
Fired when the open state changes.
Fired after the value is committed.
Fired when the component closes.
Fired when the model value changes.