Skip to content
RTL Support: Full
Accessibility: Full
Translations: Full
Migration Guide: Not Needed

Date Time Picker

A component for selecting a date and a time together, with optional AM/PM and timezone support.

Basic Usage

A date input, a time input, and a calendar-plus-time panel. The value is a timestamp in milliseconds.

vue
<template>
  <HLDateTimePicker v-model:value="value" format="yyyy-MM-dd HH:mm:ss" />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref<number | null>(null)
</script>

Controlled Value

Bind the selection with v-model:value. Both inputs and the panel read from the same timestamp, so picking a day keeps the time and picking an hour keeps the day.

Current value: 1/15/2026, 2:30:00 PM

vue
<template>
  <HLDateTimePicker v-model:value="value" format="dd / MM / yyyy HH:mm" />
  <p>Current value: {{ value ? new Date(value).toLocaleString() : 'none' }}</p>
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

// 2026-01-15 14:30:00 local time
const value = ref<number | null>(new Date('2026-01-15T14:30:00').getTime())
</script>

Manual Input

Both inputs accept typed text. A typed date keeps the existing time and a typed time keeps the existing date — only the half you typed is replaced. Nothing is committed while the text is still unparseable, and abandoning half-typed text restores the last valid display on blur.

Format

format is a date-fns format string covering the whole value. It is split into a date half and a time half at the first time token (h, H, m, s, or a) rather than at the first space, so date formats that contain spaces of their own (dd / MM / yyyy, MMMM dd, yyyy) split correctly.

The date half drives the date input's display; the time half drives both the time input and which columns the time panel shows:

  • an uppercase H shows the hours column in 24-hour format, counting 023
  • a lowercase h shows the hours column in 12-hour format, counting 112
  • an m shows the minutes column
  • an s shows the seconds column
  • an a turns on the AM/PM selector even without showAMPM

INFO

format is read once, when the picker is created. A picker keeps the format it started with, so changing format on a picker already on screen has no effect. To offer a choice of formats, give each one its own picker, or re-create the picker with a key.

Each picker below is seeded with the same moment (15 Jan 2026, 2:30 PM) so the formats can be compared directly.

vue
<template>
  <!-- ISO date, 24-hour with seconds (the default) -->
  <HLDateTimePicker v-model:value="value" format="yyyy-MM-dd HH:mm:ss" />
  <!-- Spaces inside the date half split correctly -->
  <HLDateTimePicker v-model:value="value" format="dd / MM / yyyy HH:mm" />
  <!-- A comma inside the date half, 12-hour time with AM/PM -->
  <HLDateTimePicker v-model:value="value" format="MMMM dd, yyyy hh:mm a" />
  <!-- No seconds column -->
  <HLDateTimePicker v-model:value="value" format="yyyy/MM/dd HH:mm" />
  <!-- 12-hour with seconds -->
  <HLDateTimePicker v-model:value="value" format="dd / MM / yyyy hh:mm:ss a" />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref(new Date('2026-01-15T14:30:00').getTime())
</script>

With AM/PM

Set showAMPM to add a dedicated AM/PM selector to the input row. A format containing the a token turns the selector on by itself, and when the selector is showing, the a is stripped from the time input's text so the meridiem is not displayed twice. The a is still honoured in @update:formatted-value.

vue
<template>
  <HLDateTimePicker v-model:value="value" format="dd / MM / yyyy hh:mm a" :showAMPM="true" />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref(new Date('2026-01-15T14:30:00').getTime())
</script>

INFO

The selector changes the value, not just the label. Switching it shifts the hour by 12 and emits @update:value. The binding is two-way: setting value to an hour ≥ 12 switches the selector to PM.

With Timezone

Pass timezones to add a timezone selector to the row. The zone flagged default: true is preselected on mount (falling back to the first entry), and @update:timezone announces it on mount as well as on every change.

Select Timezone

Selected timezone: none

vue
<template>
  <HLDateTimePicker
    v-model:value="value"
    format="dd / MM / yyyy hh:mm a"
    :showAMPM="true"
    :timezones="timezones"
    @update:timezone="handleTimezoneChange"
  />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import type { HLDateTimePickerTimezone } from '@platform-ui/highrise'
import { ref } from 'vue'
import { timezones } from './options'

const value = ref(new Date('2026-01-15T14:30:00').getTime())
const selectedTimezone = ref<HLDateTimePickerTimezone | null>(null)

const handleTimezoneChange = (timezone: HLDateTimePickerTimezone | null) => {
  selectedTimezone.value = timezone
  // Apply your own timezone conversion here
}
</script>
ts
import type { HLDateTimePickerTimezone } from '@platform-ui/highrise'

export const timezones: HLDateTimePickerTimezone[] = [
  { label: 'Eastern Time', value: 'America/New_York', default: true },
  { label: 'Central Time', value: 'America/Chicago' },
  { label: 'Mountain Time', value: 'America/Denver' },
  { label: 'Pacific Time', value: 'America/Los_Angeles' },
  { label: 'UTC', value: 'UTC' },
]

WARNING

Changing the timezone does not shift the timestamp. The value stays a local wall-clock moment and the zone travels alongside it, so you decide what the zone means when you persist or convert the value.

Clearable

Set clearable to add a clear control to both inputs. Clearing from either one nulls the whole value and empties both — the two halves are one timestamp, so there is no "clear just the time".

vue
<template>
  <HLDateTimePicker v-model:value="value" format="dd / MM / yyyy HH:mm" clearable />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref(new Date('2026-01-15T14:30:00').getTime())
</script>

With CTA Buttons

Show Cancel/Confirm action buttons in the panel with showCTA.

Committed value: 1/15/2026, 9:00:00 AM

vue
<template>
  <HLDateTimePicker v-model:value="value" format="dd / MM / yyyy HH:mm" showCTA />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref(new Date('2026-01-15T09:00:00').getTime())
</script>

By default (updateValueOnConfirm is true) nothing is emitted while you move around the calendar and the time columns. @update:value fires once, on Confirm — or with the original value on Cancel, so the parent never sees a value the user backed out of.

Emit on Every Change

Set :updateValueOnConfirm="false" to emit on every selection while still showing the action buttons. Cancel then emits again to undo it.

Live value: 1/15/2026, 9:00:00 AM

vue
<template>
  <HLDateTimePicker
    v-model:value="value"
    format="dd / MM / yyyy HH:mm"
    showCTA
    :updateValueOnConfirm="false"
  />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref(new Date('2026-01-15T09:00:00').getTime())
</script>

INFO

updateValueOnConfirm has no effect when showCTA is false — without the action buttons, selections always emit immediately.

With Shortcuts

Shortcuts name a whole moment, so each one replaces both the date and the time. Hovering (or focusing) a shortcut previews it in the inputs and the panel; leaving puts back what was there.

vue
<template>
  <HLDateTimePicker v-model:value="value" format="dd / MM / yyyy HH:mm" :shortcuts="shortcuts" />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'
import { shortcuts } from './options'

const value = ref<number | null>(null)
</script>
ts
export const shortcuts = {
  Now: () => Date.now(),
  'Tomorrow 9 AM': () => {
    const date = new Date()
    date.setDate(date.getDate() + 1)
    date.setHours(9, 0, 0, 0)
    return date.getTime()
  },
  'End of day': () => {
    const date = new Date()
    date.setHours(17, 0, 0, 0)
    return date.getTime()
  },
}

WARNING

Selecting a shortcut commits immediately and closes the panel, even when showCTA is on — it bypasses Confirm, so @update:value fires without a @confirm.

Disabled

Pass disabled as a boolean to disable the whole component — both inputs, the AM/PM selector, the timezone selector, and the panel no longer opens on click.

Select Timezone
vue
<template>
  <HLDateTimePicker
    v-model:value="value"
    format="dd / MM / yyyy hh:mm a"
    :showAMPM="true"
    :timezones="timezones"
    :disabled="true"
  />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'
import { timezones } from './options'

const value = ref(new Date('2026-01-15T14:30:00').getTime())
</script>

Disabled Dates

isDateDisabled receives a timestamp and returns true for days that cannot be selected. A disabled day disables all of its times too, so the time columns grey out whenever the selected day is itself unavailable. Here weekends are closed.

vue
<template>
  <HLDateTimePicker v-model:value="value" format="dd / MM / yyyy HH:mm" :isDateDisabled="isWeekend" />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref<number | null>(null)

// Weekends are not selectable
const isWeekend = (timestamp: number) => {
  const day = new Date(timestamp).getDay()
  return day === 0 || day === 6
}
</script>

Disabled Times

isTimeDisabled decides which times are selectable on a given day. It receives the currently selected date and returns an object of rules — any of hours, minutes, seconds — so the available times can change as you move between days. Omitted rules leave that column fully enabled.

This example models clinic hours: weekends closed, 9–5 on weekdays, a half day on Fridays, and appointments starting on the hour or the half hour. Pick a Friday and the afternoon greys out.

vue
<template>
  <HLDateTimePicker
    v-model:value="value"
    format="dd / MM / yyyy HH:mm"
    showCTA
    :isDateDisabled="isWeekend"
    :isTimeDisabled="clinicHours"
  />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'
import { isWeekend, clinicHours } from './options'

const value = ref<number | null>(null)
</script>
ts
import type { HLDateTimePickerTimeRules } from '@platform-ui/highrise'

// Weekends are closed
export const isWeekend = (timestamp: number) => {
  const day = new Date(timestamp).getDay()
  return day === 0 || day === 6
}

// Opening hours depend on the selected day
export const clinicHours = (date: number): HLDateTimePickerTimeRules => {
  // Exclusive: the clinic closes at this hour, so it is not itself bookable.
  const closingHour = new Date(date).getDay() === 5 ? 13 : 17
  return {
    hours: hour => hour < 9 || hour >= closingHour,
    // Appointments start on the hour or the half hour
    minutes: minute => minute % 30 !== 0,
  }
}

INFO

When a disabled time is selected the panel substitutes the nearest valid one and emits @update:fallback with the requested value, the applied value, and a reason.

Limit Selectable Years

Use minYear and maxYear to bound the year range shown in the year view.

vue
<template>
  <HLDateTimePicker
    v-model:value="value"
    format="dd / MM / yyyy HH:mm"
    :minYear="2024"
    :maxYear="2030"
  />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref<number | null>(null)
</script>

Sizes

Set size to scale the whole input row; it accepts lg, md, sm, xs, 2xs, and 3xs.

vue
<template>
  <HLDateTimePicker v-model:value="value" size="lg" format="dd / MM / yyyy HH:mm" />
  <HLDateTimePicker v-model:value="value" size="md" format="dd / MM / yyyy HH:mm" />
  <HLDateTimePicker v-model:value="value" size="sm" format="dd / MM / yyyy HH:mm" />
  <HLDateTimePicker v-model:value="value" size="xs" format="dd / MM / yyyy HH:mm" />
  <HLDateTimePicker v-model:value="value" size="2xs" format="dd / MM / yyyy HH:mm" />
  <HLDateTimePicker v-model:value="value" size="3xs" format="dd / MM / yyyy HH:mm" />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref<number | null>(null)
</script>

Container Widths

The row fills whatever container it is given: the date and time inputs share the space that is left, the AM/PM selector stays a fixed width, and the timezone selector grows to take up the remainder. No per-container props are involved.

640px — full form row

Select Timezone
vue
<template>
  <div style="width: 640px">
    <HLDateTimePicker format="dd / MM / yyyy hh:mm a" :showAMPM="true" :timezones="timezones" />
  </div>
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { timezones } from './options'
</script>

Custom Prefix Icons

datePrefix and timePrefix are separate slots, so each icon can be replaced on its own — overriding one leaves the other's default in place.

vue
<template>
  <HLDateTimePicker format="dd / MM / yyyy hh:mm a" :showAMPM="true">
    <template #datePrefix>
      <CalendarPlus01Icon />
    </template>
    <template #timePrefix>
      <AlarmClockIcon />
    </template>
  </HLDateTimePicker>
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { AlarmClockIcon, CalendarPlus01Icon } from '@gohighlevel/ghl-icons/24/outline'
</script>

Custom Placeholders

Pass placeholder as an object to override the date, time, and timezone placeholders individually. Anything you leave out keeps its translated default.

Select Timezone
vue
<template>
  <HLDateTimePicker
    format="dd / MM / yyyy hh:mm a"
    :showAMPM="true"
    :timezones="timezones"
    :placeholder="{ date: 'DD / MM / YYYY', time: 'hh:mm' }"
  />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { timezones } from './options'
</script>

Teleport Target

By default the panel is teleported to <body>, which keeps it above other content but detaches it from scrolling containers. Point to at a CSS selector or element to mount the panel inside a specific container so it stays anchored on scroll, or pass false to render it in place.

vue
<template>
  <div id="scroll-container" style="position: relative; height: 220px; overflow: auto;">
    <HLDateTimePicker v-model:value="value" format="dd / MM / yyyy HH:mm" to="#scroll-container" />
  </div>
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref<number | null>(null)
</script>

INFO

If the trigger moves after the panel has opened (a resize, a layout shift), call the exposed syncPosition() method through a template ref to re-anchor it.

Form Validation

Wrap the picker in an HLFormItem and drive validation from HLForm's :model and :rules. Bind the field with v-model:value and set the item's path to the model key. Both the date and the time input pick up the item's error state.

vue
<template>
  <HLForm :model="formModel" :rules="formRules" label-placement="top">
    <HLFormItem label="Appointment" path="appointment">
      <HLDateTimePicker v-model:value="formModel.appointment" format="dd / MM / yyyy HH:mm" clearable />
    </HLFormItem>
  </HLForm>
</template>

<script setup lang="ts">
import { HLDateTimePicker, HLForm, HLFormItem } from '@platform-ui/highrise'
import { rules as formRules } from './options'
import { ref } from 'vue'

const formModel = ref({ appointment: null })
</script>
ts
// Naive expects a sync validator to RETURN an Error to fail (not throw).
export const rules = {
  appointment: {
    required: true,
    trigger: ['blur', 'change'],
    validator: (_rule, value: number | null) => {
      if (!value) return new Error('Please select a date and time')
      if (value < Date.now()) return new Error('Appointment cannot be in the past')
      return true
    },
  },
}

Controlling Panel Visibility

Leave show undefined and the picker owns its open state — clicking either input opens the panel, and clicking outside or pressing Escape closes it. Pass a boolean and the consumer takes over: the picker's own click history is ignored and the panel follows the prop.

vue
<template>
  <HLDateTimePicker
    v-model:value="value"
    :show="isOpen"
    format="dd / MM / yyyy HH:mm"
    @update:show="isOpen = $event"
  />
</template>

<script setup lang="ts">
import { HLDateTimePicker } from '@platform-ui/highrise'
import { ref } from 'vue'

const value = ref<number | null>(null)
const isOpen = ref(false)
</script>

Event Testing

This example logs the events the picker emits as you interact with it. Try the following:

  • Pick a date or a time to test @update:value and @update:formatted-value
  • Open and close the panel to test @update:show
  • Click Confirm or Cancel to test @confirm and @cancel
  • Clear either input to test @clear
  • Change the timezone to test @update:timezone
  • Select a disabled hour to trigger @update:fallback
Select Timezone

Event Log:

No events logged yet. Interact with the picker above.
vue
<template>
  <HLDateTimePicker
    v-model:value="value"
    format="dd / MM / yyyy hh:mm a"
    clearable
    showCTA
    :showAMPM="true"
    :timezones="timezones"
    :isTimeDisabled="() => ({ hours: hour => hour < 9 || hour > 17 })"
    @update:value="val => addEventLog('@update:value → ' + val)"
    @update:formatted-value="val => addEventLog('@update:formatted-value → ' + val)"
    @update:show="val => addEventLog('@update:show → ' + val)"
    @confirm="(formatted, raw) => addEventLog('@confirm → ' + formatted)"
    @cancel="(formatted, raw) => addEventLog('@cancel → ' + formatted)"
    @clear="addEventLog('@clear')"
    @update:timezone="tz => addEventLog('@update:timezone → ' + (tz ? tz.value : 'null'))"
    @update:fallback="info => addEventLog('@update:fallback → ' + info.reason)"
  />
  <div>
    <p>Event Log:</p>
    <div v-for="(log, index) in eventLog" :key="index">{{ log.timestamp }}: {{ log.event }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { HLDateTimePicker } from '@platform-ui/highrise'
import { timezones } from './options'

const value = ref<number | null>(null)
const eventLog = ref<{ event: string; timestamp: string }[]>([])
const addEventLog = (event: string) => {
  eventLog.value.unshift({ event, timestamp: new Date().toLocaleTimeString() })
  if (eventLog.value.length > 5) {
    eventLog.value.pop()
  }
}
</script>

Design Guidelines

Input components use a box-shadow to render their focus ring. Box-shadows render outside the element's bounds and may be clipped by any ancestor using overflow: hidden (e.g. Tab Panels or Dropdown Menus).

To prevent this, add a small gutter padding to the component's wrapper to ensure there is enough room for the focus ring to render without being cut off.

vue
<div class="p-[3px]">
  <!-- Your component here -->
</div>

Accessibility

  • Both inputs are exposed as combobox triggers with aria-haspopup="dialog", and aria-expanded tracks the panel.
  • While the panel is open, aria-controls points both inputs at it. Each picker gets its own panel id, so several pickers on a page cannot collide.
  • The panel is a role="dialog" labelled from the translated "choose date" string; the shortcut row and the action row are labelled role="group"s.
  • Escape closes the panel from within it, cancelling the draft when a CTA is shown.
  • Connect the picker to its own label via aria-labelledby / aria-label, and surface format hints through aria-describedby. Inside an HLFormItem the feedback text is wired to both inputs automatically.

Imports

ts
import { HLDateTimePicker } from '@platform-ui/highrise'

Props

NameTypeDefaultDescription
idstringundefinedId applied to the picker's root element
type'datetime''datetime'Type of the picker. Only datetime is available today; datetimerange is planned
size'lg' | 'md' | 'sm' | 'xs' | '2xs' | '3xs''md'Size of the input row
formatstring'yyyy-MM-dd HH:mm:ss'date-fns format for the whole value. It is split at the first time token (h, H, m, s, a); the time half decides which columns the panel shows
valuenumber | nullnullControlled value as a timestamp in milliseconds. Bind with v-model:value or pass value + @update:value
placeholderHLDateTimePickerPlaceholderstranslated defaultsPer-field placeholder text (date, time, timezone). Fields you leave out keep their translated default
disabledbooleanfalseDisables both inputs, the AM/PM selector, the timezone selector, and stops the panel from opening. All-or-nothing — individual fields cannot be disabled separately
clearablebooleanfalseShow a clear control on both inputs. Clearing either one nulls the whole value
placementHLPopoverPlacementundefinedPlacement of the panel relative to the trigger. When unset it resolves to 'bottom-start' (LTR) or 'bottom-end' (RTL)
shortcutsRecord<string, number | (() => number)>undefinedQuick selections. Each shortcut names a whole moment and replaces both the date and the time
showAMPMbooleanfalseShow the AM/PM selector. An a token in format turns it on as well
showCTAbooleanfalseShow the Cancel/Confirm action buttons in the panel
updateValueOnConfirmbooleantrueWith showCTA, holds update:value back until Confirm is pressed so the parent never sees a cancelled value. Set false to emit on every change. No effect when showCTA is false
showbooleanundefinedControls panel visibility. Leave undefined for the picker to manage its own open state
tostring | HTMLElement | falseundefinedTeleport target for the panel. Pass a CSS selector or HTMLElement to mount it inside a specific container, or false to disable teleporting
timezonesHLDateTimePickerTimezone[][]Timezone options. A non-empty array adds the timezone selector to the row
minYearnumberundefinedLowest selectable year
maxYearnumberundefinedHighest selectable year
isDateDisabled(timestamp: number) => booleanundefinedWhole days that cannot be selected; return true to disable. A disabled day disables all of its times too
isTimeDisabled(date: number) => HLDateTimePickerTimeRules | undefinedundefinedWhich times are selectable on a given day. Receives the selected date, so the rules can differ per day

Type Examples

ts
// A single moment as a timestamp in milliseconds
const appointment: number = new Date('2026-01-15T14:30:00').getTime()

Interfaces

HLDateTimePickerPlaceholders

ts
interface HLDateTimePickerPlaceholders {
  date?: string // Placeholder for the date input
  time?: string // Placeholder for the time input
  timezone?: string // Placeholder for the timezone selector
}

HLDateTimePickerTimeRules

ts
interface HLDateTimePickerTimeRules {
  hours?: (hour: number) => boolean
  minutes?: (minute: number, hour: number) => boolean
  seconds?: (second: number, hour: number, minute: number) => boolean
}

HLDateTimePickerTimezone

ts
interface HLDateTimePickerTimezone {
  label: string // Display label for the timezone
  value: string // Timezone value (e.g., 'America/Los_Angeles')
  default?: boolean // Preselected on mount. Without one, the first zone in the list is used
}

HLTimePickerFallbackInfo

ts
interface HLTimePickerFallbackInfo {
  reason: 'disabled-input' | 'all-times-disabled' | 'disabled-ampm-toggle'
  requestedValue: { hour: number; minute: number; second: number }
  fallbackValue: { hour: number; minute: number; second: number } | null
  message: string
}

Emits

NameParametersDescription
@update:value(value: number | null) => voidEmitted when the value changes, as a timestamp in milliseconds
@update:formatted-value(value: string | null) => voidEmitted alongside update:value with the whole value rendered through format
@update:show(value: boolean) => voidEmitted when panel visibility changes
@confirm(value: string | null, rawValue: number | null) => voidEmitted when Confirm is pressed. Both arguments describe the accepted moment
@cancel(value: string | null, rawValue: number | null) => voidEmitted when Cancel is pressed (or Escape with a CTA on screen). value is the discarded draft, rawValue is the value being restored
@clear() => voidEmitted when either input is cleared
@update:timezone(timezone: HLDateTimePickerTimezone | null) => voidEmitted on mount with the initial zone, and on every zone change
@update:fallback(info: HLTimePickerFallbackInfo) => voidEmitted when a disabled time was requested and the panel substituted the nearest valid one
@update:ampm(ampm: 'AM' | 'PM') => voidEmitted when the AM/PM selector changes. While a value is set this repeats what update:value already says; with an empty picker it is the only signal

Slots

NameDescription
datePrefixReplaces the calendar icon on the date input
timePrefixReplaces the clock icon on the time input

Methods

Accessed through a template ref on the component.

MethodDescription
syncPosition()Recalculates and updates the panel's position. Useful after a resize or a layout shift.