Usage
Bind the selected value with v-model. It is a CalendarDateTime or a ZonedDateTime from @internationalized/date, the same types Calendar and InputDate use.
Picking a date moves to the time step; picking a minute closes the picker.
Date only
Use date-only when the time is not part of the answer. The time step is dropped and the value stays a CalendarDate, so it carries no time at all rather than a time of midnight.
Presets
A preset column sits beside the calendar with today, tomorrow, the end of the week, a week out and the end of the month. Pass presets to replace the list, or hide-presets to drop the column.
A preset's value may be a function, which is resolved when the list renders — so a relative preset stays correct however long the page has been open.
Minute step
Use minute-step to change how many minutes the grid moves by. It is clamped to 1…30, and a value that is not a finite number falls back to 5.
<template>
<B24DateTimePicker :minute-step="15" placeholder="Pick a date and time" />
</template>
Clock
The grid always runs 00–23, so under a locale that prefers a 12-hour clock the
trigger would say 2:30 PM beside a cell marked 14. Use hour12 to settle it.
Omit the prop to follow the locale.
<script setup lang="ts">
import { CalendarDateTime } from '@internationalized/date'
const value = shallowRef(new CalendarDateTime(2024, 10, 6, 14, 30))
</script>
<template>
<B24DateTimePicker v-model="value" placeholder="Pick a date and time" />
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import { CalendarDateTime } from '@internationalized/date'
const value = shallowRef(new CalendarDateTime(2024, 10, 6, 14, 30))
</script>
<template>
<B24DateTimePicker v-model="value" placeholder="Pick a date and time" />
</template>
Custom trigger
The #default slot replaces the trigger. It receives the open state, the value and the formatted value.
Inside a FormField
API
Props
Slots
Emits
Theme
export default {
slots: {
content: 'p-0 overflow-hidden',
body: 'flex flex-col-reverse sm:flex-row',
trigger: 'cursor-pointer w-full',
main: 'flex flex-col p-4',
presets: 'flex flex-row sm:flex-col gap-1.5 p-4 sm:border-l sm:border-(--ui-color-divider-default) overflow-x-auto sm:overflow-x-visible sm:overflow-y-auto sm:max-h-[var(--max-height-popup-menu)]',
preset: 'flex flex-col items-start text-start min-w-36 px-3 py-2 sm:py-1 rounded-(--ui-border-radius-md) border border-(--ui-color-divider-default) cursor-pointer select-none transition-colors hover:bg-(--ui-color-bg-content-secondary) focus-visible:outline-(--ui-color-design-outline-focused-stroke) focus-visible:outline-1 data-[active=true]:border-(--b24ui-background) data-[active=true]:text-(--b24ui-background)',
presetLabel: 'text-(length:--ui-font-size-md) font-(--ui-font-weight-medium) text-(--b24ui-typography-label-color)',
presetHint: 'text-(length:--ui-font-size-xs) text-(--ui-color-design-plain-na-content-secondary)',
timeHeader: 'flex items-center gap-1 pb-3',
timeHeaderBack: 'inline-flex items-center justify-center shrink-0 size-11 sm:size-7 rounded-(--ui-border-radius-circle) cursor-pointer transition-colors hover:bg-(--ui-color-bg-content-secondary) focus-visible:outline-(--ui-color-design-outline-focused-stroke) focus-visible:outline-1',
timeHeaderBackIcon: 'size-5 text-(--ui-color-design-plain-na-content-secondary)',
timeHeaderLabel: 'flex-1 min-w-0 text-center pe-7 text-legend font-(--ui-font-weight-semi-bold) block truncate p-1.5',
timeBody: 'flex justify-center min-w-[252px]',
timeColumn: 'flex flex-col gap-1 px-3 first:pl-1 last:pr-1 border-l border-(--ui-color-divider-default) first:border-l-0',
timeColumnTitle: 'text-center pb-2 text-(length:--ui-font-size-xs) text-(--ui-color-design-plain-na-content-secondary)',
timeHoursGrid: 'grid grid-cols-4 gap-1',
timeMinutesGrid: 'grid grid-cols-2 gap-1',
timeCell: 'inline-flex items-center justify-center size-11 sm:size-7 rounded-(--ui-border-radius-circle) text-label cursor-pointer select-none transition focus-visible:ring-2 focus:outline-none focus-visible:ring-(--b24ui-background-hover) data-selected:bg-(--b24ui-background) data-selected:text-(--b24ui-color) data-selected:focus-visible:ring-(--b24ui-background-hover) data-[now]:not-data-selected:not-hover:text-(--b24ui-background) data-[now]:font-(--ui-font-weight-semi-bold) hover:not-data-selected:bg-(--b24ui-background) hover:not-data-selected:text-(--b24ui-color)',
footer: 'mt-2 self-start inline-flex items-center gap-1.5 px-2 py-2 sm:py-1 rounded-(--ui-border-radius-md) text-(--b24ui-background) cursor-pointer select-none transition-colors hover:bg-(--ui-color-bg-content-secondary) focus-visible:outline-(--ui-color-design-outline-focused-stroke) focus-visible:outline-1',
footerIcon: 'size-4 shrink-0',
footerValue: 'text-(length:--ui-font-size-sm) font-(--ui-font-weight-medium)'
},
variants: {
color: {
'air-primary': {
content: 'style-filled'
},
'air-primary-success': {
content: 'style-filled-success'
},
'air-primary-alert': {
content: 'style-filled-alert'
},
'air-primary-copilot': {
content: 'style-filled-copilot'
},
'air-primary-warning': {
content: 'style-filled-warning'
}
},
size: {
xs: {
timeHeaderLabel: 'text-(length:--ui-font-size-md)',
timeColumnTitle: 'text-(length:--ui-font-size-4xs)',
timeCell: 'sm:size-6 text-(length:--ui-font-size-sm)',
preset: 'min-w-32 px-2 py-1'
},
sm: {
timeHeaderLabel: 'text-(length:--ui-font-size-md)',
timeColumnTitle: 'text-(length:--ui-font-size-3xs)',
timeCell: 'sm:size-6 text-(length:--ui-font-size-sm)',
preset: 'min-w-34 px-2.5 py-1'
},
md: {
timeHeaderLabel: 'text-(length:--ui-font-size-lg)',
timeColumnTitle: 'text-(length:--ui-font-size-xs)',
timeCell: 'text-(length:--ui-font-size-md)'
},
lg: {
timeHeaderLabel: 'text-(length:--ui-font-size-2xl)',
timeColumnTitle: 'text-(length:--ui-font-size-xs)',
timeCell: 'sm:size-8 text-(length:--ui-font-size-lg)',
preset: 'min-w-40 px-3.5 py-2'
}
}
},
defaultVariants: {
size: 'md',
color: 'air-primary'
}
}