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.

Meeting time
Selected: 14:30
vue
<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.

Nightly backup runs at
UTC, every day at 02:00
vue
<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.

Book a class
vue
<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.

Shift hours
From
To
Working 09:00 – 17:00
vue
<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
typescript
/**
 * 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
}
modelValue
= ""
string

Controlled value, canonical `HH:mm` (or `HH:mm:ss`).

interval
= 15
number

Minute interval between generated options.

options
= []
{ value: string; label?: string; }[]

Caller-provided option values; bypasses the generated grid.

side
PopoverSide

Preferred popover side.

align
PopoverAlign

Alignment of the popover along the trigger edge.

offset
number

Gap between trigger and popover in pixels.

placeholder
= "Select time"
string

Placeholder text when no value is selected.

variant
= "subtle" as Variant
InputVariant

Visual style variant.

size
InputSize

Visual size of the trigger input. Forwarded to the underlying `TextInput`.

typeable
= true
boolean

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`.

keepOpen
boolean

Keeps the popover open after a time is selected.

format
string

Dayjs format string used for display. Default: `HH:mm`.

disabled
= false
boolean

Disable the time picker.

open
boolean

Controlled popover open state. Use with `v-model:open` for two-way binding.

openOnFocus
= false
boolean

Opens the popover when the input receives focus. Default: false.

openOnClick
= true
boolean

Opens the popover when the input is clicked. Default: true.

min
string

Minimum selectable time as `HH:mm[:ss]`.

max
string

Maximum selectable time as `HH:mm[:ss]`.

label
string

Label rendered above (or beside, for binary controls) the input.

description
string

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
ErrorMessageValue

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.

required
boolean

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.

id
string

HTML id of the underlying control. Auto-generated via `useId()` if omitted.

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.

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.

open
() => void

Opens the panel. A disabled picker stays closed.

close
() => void

Closes the panel.

focus
(options?: FocusOptions) => void

Moves focus to the input.