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.
<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
<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
Hshows the hours column in 24-hour format, counting0–23 - a lowercase
hshows the hours column in 12-hour format, counting1–12 - an
mshows the minutes column - an
sshows the seconds column - an
aturns on the AM/PM selector even withoutshowAMPM
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.
<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.
<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.
Selected timezone: none
<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>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".
<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
<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
<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.
<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>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.
<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.
<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.
<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>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.
<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.
<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
<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.
<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.
<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.
<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.
<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>// 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.
<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:valueand@update:formatted-value - Open and close the panel to test
@update:show - Click Confirm or Cancel to test
@confirmand@cancel - Clear either input to test
@clear - Change the timezone to test
@update:timezone - Select a disabled hour to trigger
@update:fallback
Event Log:
<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.
<div class="p-[3px]">
<!-- Your component here -->
</div>Accessibility
- Both inputs are exposed as
comboboxtriggers witharia-haspopup="dialog", andaria-expandedtracks the panel. - While the panel is open,
aria-controlspoints 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 labelledrole="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 througharia-describedby. Inside anHLFormItemthe feedback text is wired to both inputs automatically.
Imports
import { HLDateTimePicker } from '@platform-ui/highrise'Props
| Name | Type | Default | Description |
|---|---|---|---|
| id | string | undefined | Id 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 |
| format | string | '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 |
| value | number | null | null | Controlled value as a timestamp in milliseconds. Bind with v-model:value or pass value + @update:value |
| placeholder | HLDateTimePickerPlaceholders | translated defaults | Per-field placeholder text (date, time, timezone). Fields you leave out keep their translated default |
| disabled | boolean | false | Disables both inputs, the AM/PM selector, the timezone selector, and stops the panel from opening. All-or-nothing — individual fields cannot be disabled separately |
| clearable | boolean | false | Show a clear control on both inputs. Clearing either one nulls the whole value |
| placement | HLPopoverPlacement | undefined | Placement of the panel relative to the trigger. When unset it resolves to 'bottom-start' (LTR) or 'bottom-end' (RTL) |
| shortcuts | Record<string, number | (() => number)> | undefined | Quick selections. Each shortcut names a whole moment and replaces both the date and the time |
| showAMPM | boolean | false | Show the AM/PM selector. An a token in format turns it on as well |
| showCTA | boolean | false | Show the Cancel/Confirm action buttons in the panel |
| updateValueOnConfirm | boolean | true | With 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 |
| show | boolean | undefined | Controls panel visibility. Leave undefined for the picker to manage its own open state |
| to | string | HTMLElement | false | undefined | Teleport target for the panel. Pass a CSS selector or HTMLElement to mount it inside a specific container, or false to disable teleporting |
| timezones | HLDateTimePickerTimezone[] | [] | Timezone options. A non-empty array adds the timezone selector to the row |
| minYear | number | undefined | Lowest selectable year |
| maxYear | number | undefined | Highest selectable year |
| isDateDisabled | (timestamp: number) => boolean | undefined | Whole days that cannot be selected; return true to disable. A disabled day disables all of its times too |
| isTimeDisabled | (date: number) => HLDateTimePickerTimeRules | undefined | undefined | Which times are selectable on a given day. Receives the selected date, so the rules can differ per day |
Type Examples
// A single moment as a timestamp in milliseconds
const appointment: number = new Date('2026-01-15T14:30:00').getTime()Interfaces
HLDateTimePickerPlaceholders
interface HLDateTimePickerPlaceholders {
date?: string // Placeholder for the date input
time?: string // Placeholder for the time input
timezone?: string // Placeholder for the timezone selector
}HLDateTimePickerTimeRules
interface HLDateTimePickerTimeRules {
hours?: (hour: number) => boolean
minutes?: (minute: number, hour: number) => boolean
seconds?: (second: number, hour: number, minute: number) => boolean
}HLDateTimePickerTimezone
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
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
| Name | Parameters | Description |
|---|---|---|
@update:value | (value: number | null) => void | Emitted when the value changes, as a timestamp in milliseconds |
@update:formatted-value | (value: string | null) => void | Emitted alongside update:value with the whole value rendered through format |
@update:show | (value: boolean) => void | Emitted when panel visibility changes |
@confirm | (value: string | null, rawValue: number | null) => void | Emitted when Confirm is pressed. Both arguments describe the accepted moment |
@cancel | (value: string | null, rawValue: number | null) => void | Emitted when Cancel is pressed (or Escape with a CTA on screen). value is the discarded draft, rawValue is the value being restored |
@clear | () => void | Emitted when either input is cleared |
@update:timezone | (timezone: HLDateTimePickerTimezone | null) => void | Emitted on mount with the initial zone, and on every zone change |
@update:fallback | (info: HLTimePickerFallbackInfo) => void | Emitted when a disabled time was requested and the panel substituted the nearest valid one |
@update:ampm | (ampm: 'AM' | 'PM') => void | Emitted 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
| Name | Description |
|---|---|
datePrefix | Replaces the calendar icon on the date input |
timePrefix | Replaces the clock icon on the time input |
Methods
Accessed through a template ref on the component.
| Method | Description |
|---|---|
syncPosition() | Recalculates and updates the panel's position. Useful after a resize or a layout shift. |