v2.14.0

DateTimePicker

A date and time selector: a calendar that hands over to an hour and minute grid, with presets beside it.

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.

<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import { CalendarDateTime } from '@internationalized/date'

const value = shallowRef<DateValue>(new CalendarDateTime(2024, 10, 6, 14, 30))
</script>

<template>
  <B24DateTimePicker v-model="value" placeholder="Pick a date and time" />
</template>

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.

<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import { CalendarDate } from '@internationalized/date'

const value = shallowRef<DateValue>(new CalendarDate(2024, 10, 6))
</script>

<template>
  <B24DateTimePicker v-model="value" date-only placeholder="Pick a date" />
</template>

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.

<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import { getLocalTimeZone, today } from '@internationalized/date'
import type { DateTimePickerPreset } from '@bitrix24/b24ui-nuxt'

const value = shallowRef<DateValue | undefined>()

// A function is resolved when the list renders, so "in three days" stays
// correct however long the page has been open.
const presets: DateTimePickerPreset[] = [
  { label: 'In three days', value: () => today(getLocalTimeZone()).add({ days: 3 }) },
  { label: 'In two weeks', value: () => today(getLocalTimeZone()).add({ weeks: 2 }) },
  { label: 'Next quarter', value: () => today(getLocalTimeZone()).add({ months: 3 }) }
]
</script>

<template>
  <B24DateTimePicker v-model="value" :presets="presets" placeholder="Pick a deadline" />
</template>

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.

<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'
import Calendar1Icon from '@bitrix24/b24icons-vue/main/Calendar1Icon'

const value = shallowRef<DateValue | undefined>()
</script>

<template>
  <B24DateTimePicker v-model="value">
    <template #default="{ formatted }">
      <B24Button :icon="Calendar1Icon" color="air-secondary-accent">
        {{ formatted || 'Choose when' }}
      </B24Button>
    </template>
  </B24DateTimePicker>
</template>

Inside a FormField

Shown to everyone on the deal
<script setup lang="ts">
import { shallowRef } from 'vue'
import type { DateValue } from '@internationalized/date'

const value = shallowRef<DateValue | undefined>()
</script>

<template>
  <B24FormField label="Due date" hint="Shown to everyone on the deal" class="w-full">
    <B24DateTimePicker v-model="value" placeholder="Not set" />
  </B24FormField>
</template>

API

Props

Prop Default Type
modelValue CalendarDate | CalendarDateTime | ZonedDateTime

The selected value. CalendarDateTime or ZonedDateTime normally, CalendarDate when dateOnly is set.

defaultValue CalendarDate | CalendarDateTime | ZonedDateTime

The value before the user picks one.

openboolean

Controlled open state. Pairs with update:open, so v-model:open works.

defaultOpenboolean

Whether the picker starts open. Ignored when open is given.

dateOnlyfalseboolean

Drop the time step. The value stays a CalendarDate, so it carries no time at all rather than a time of 00:00.

minuteStep5 number

Minutes between cells in the time grid. Clamped to 1…30; a value that is not a finite number falls back to the default rather than rendering an empty or unbounded grid.

locale string

Locale for the calendar and the formatted value. Falls back to B24App's.

placeholder string

Shown on the trigger while the value is empty.

presets DateTimePickerPreset[]

Replaces the built-in preset list.

hidePresetsfalseboolean

Drop the preset column.

format`{ dateStyle: 'medium' }`, plus `timeStyle: 'short'` unless `dateOnly` is set Intl.DateTimeFormatOptions

How the value is formatted on the trigger.

hour12boolean

Clock the formatted value uses. The grid is always 00–23, so leaving this to the locale made an en trigger read 2:30 PM beside a cell marked 14. false is the 24-hour clock; omit it to follow the locale.

color'air-primary'"air-primary" | "air-primary-success" | "air-primary-alert" | "air-primary-warning" | "air-primary-copilot"
size'md' "xs" | "md" | "sm" | "lg"
disabledfalseboolean

Blocks the trigger, so the picker cannot be opened.

iconCalendar1IconIconComponent

Leading icon on the default trigger.

timeIconClockIconIconComponent

Icon beside the time hint under the calendar.

backIconicons.chevronLeftIconComponent

Icon on the control that returns from the time step to the calendar.

popover Omit<PopoverProps<PopoverMode>, "open" | "defaultOpen" | "modelValue">

Forwarded to the B24Popover used on pointer-sized screens.

drawer Omit<DrawerProps, "open" | "defaultOpen">

Forwarded to the B24Drawer used on small screens.

calendar Omit<CalendarProps<false, false>, "modelValue" | "defaultValue" | "range" | "multiple">

Forwarded to the inner B24Calendar.

color and size cascade into it from this component unless they are given here. They are bound after the spread rather than before, because v-bind overwrites with keys that are present but undefined.

input Omit<InputProps<AcceptableValue, ModelModifiers>, "modelValue" | "defaultValue">

Forwarded to the B24Input used as the default trigger.

The input is the trigger, not a control inside one: B24Input forwards fall-through attributes onto its <input>, so the popover's aria-haspopup and aria-expanded land on a real control. Wrapping a readonly input in a clickable element instead nests one interactive control inside another, which axe rejects as nested-interactive.

b24ui { content?: SlotClass; body?: SlotClass; trigger?: SlotClass; main?: SlotClass; presets?: SlotClass; preset?: SlotClass; presetLabel?: SlotClass; presetHint?: SlotClass; timeHeader?: SlotClass; timeHeaderBack?: SlotClass; timeHeaderBackIcon?: SlotClass; timeHeaderLabel?: SlotClass; timeBody?: SlotClass; timeColumn?: SlotClass; timeColumnTitle?: SlotClass; timeHoursGrid?: SlotClass; timeMinutesGrid?: SlotClass; timeCell?: SlotClass; footer?: SlotClass; footerIcon?: SlotClass; footerValue?: SlotClass; }

Slots

Slot Type
default{ open: boolean; value?: DateValue | undefined; formatted: string; }

Replaces the trigger. Receives the open state and the formatted value.

presets{ presets: DateTimePickerPreset[]; select: (preset: DateTimePickerPreset) => void; isActive: (preset: DateTimePickerPreset) => boolean; }

Replaces the built-in preset list.

preset{ preset: DateTimePickerPreset; select: () => void; active: boolean; }

Replaces one preset button.

time-header{ value?: DateValue | undefined; formatted: string; back: () => void; }

Replaces the header of the time step.

Emits

Event Type
update:open[open: boolean]
change[value: DateValue | undefined]
update:modelValue[value: DateValue | undefined]

Theme

https://github.com/bitrix24/b24ui/tree/main/src/theme/date-time-picker.ts
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'
  }
}