---
title: "DateTimePicker"
description: "A date and time selector: a calendar that hands over to an hour and minute grid, with presets beside it."
canonical_url: "https://bitrix24.github.io/b24ui/docs/components/date-time-picker"
---
# 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`](https://react-spectrum.adobe.com/internationalized/date/index.html), the same types [Calendar](https://bitrix24.github.io/b24ui/raw/docs/components/calendar.md) and [InputDate](https://bitrix24.github.io/b24ui/raw/docs/components/input-date.md) use.

Picking a date moves to the time step; picking a minute closes the picker.

```vue [DateTimePickerBasicExample.vue]
<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.

```vue [DateTimePickerDateOnlyExample.vue]
<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.

```vue [DateTimePickerCustomPresetsExample.vue]
<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`.

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

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

const value = shallowRef(new CalendarDateTime(undefined, undefined, undefined, undefined, undefined))
</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.

```vue [DateTimePickerCustomTriggerExample.vue]
<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

```vue [DateTimePickerFormFieldExample.vue]
<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

```ts
/**
 * Props for the DateTimePicker component
 */
interface DateTimePickerProps {
  /**
   * The selected value. `CalendarDateTime` or `ZonedDateTime` normally,
   * `CalendarDate` when `dateOnly` is set.
   */
  modelValue?: CalendarDate | CalendarDateTime | ZonedDateTime | undefined;
  /**
   * The value before the user picks one.
   */
  defaultValue?: CalendarDate | CalendarDateTime | ZonedDateTime | undefined;
  /**
   * Controlled open state. Pairs with `update:open`, so `v-model:open` works.
   */
  open?: boolean | undefined;
  /**
   * Whether the picker starts open. Ignored when `open` is given.
   */
  defaultOpen?: boolean | undefined;
  /**
   * Drop the time step. The value stays a `CalendarDate`, so it carries no
   * time at all rather than a time of `00:00`.
   * @default false
   */
  dateOnly?: boolean | undefined;
  /**
   * 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.
   * @default 5
   */
  minuteStep?: number | undefined;
  /**
   * Locale for the calendar and the formatted value. Falls back to `B24App`'s.
   */
  locale?: string | undefined;
  /**
   * Shown on the trigger while the value is empty.
   */
  placeholder?: string | undefined;
  /**
   * Replaces the built-in preset list.
   */
  presets?: DateTimePickerPreset[] | undefined;
  /**
   * Drop the preset column.
   * @default false
   */
  hidePresets?: boolean | undefined;
  /**
   * How the value is formatted on the trigger.
   * @default `{ dateStyle: 'medium' }`, plus `timeStyle: 'short'` unless `dateOnly` is set
   */
  format?: Intl.DateTimeFormatOptions | undefined;
  /**
   * 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.
   */
  hour12?: boolean | undefined;
  /**
   * @default 'air-primary'
   */
  color?: "air-primary" | "air-primary-success" | "air-primary-alert" | "air-primary-warning" | "air-primary-copilot";
  /**
   * @default 'md'
   */
  size?: "xs" | "md" | "sm" | "lg" | undefined;
  /**
   * Blocks the trigger, so the picker cannot be opened.
   * @default false
   */
  disabled?: boolean | undefined;
  /**
   * Leading icon on the default trigger.
   * @default Calendar1Icon
   */
  icon?: (props: HTMLAttributes & VNodeProps & {}, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<...>>(exposed?: Exposed | undefined) => void; }, "expose">): any | undefined;
  /**
   * Icon beside the time hint under the calendar.
   * @default ClockIcon
   */
  timeIcon?: (props: HTMLAttributes & VNodeProps & {}, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<...>>(exposed?: Exposed | undefined) => void; }, "expose">): any | undefined;
  /**
   * Icon on the control that returns from the time step to the calendar.
   * @default icons.chevronLeft
   */
  backIcon?: (props: HTMLAttributes & VNodeProps & {}, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<...>>(exposed?: Exposed | undefined) => void; }, "expose">): any | undefined;
  /**
   * Forwarded to the `B24Popover` used on pointer-sized screens.
   */
  popover?: Omit<PopoverProps<PopoverMode>, "open" | "defaultOpen" | "modelValue"> | undefined;
  /**
   * Forwarded to the `B24Drawer` used on small screens.
   */
  drawer?: Omit<DrawerProps, "open" | "defaultOpen"> | undefined;
  /**
   * 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`.
   */
  calendar?: Omit<CalendarProps<false, false>, "modelValue" | "defaultValue" | "range" | "multiple"> | undefined;
  /**
   * 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`.
   */
  input?: Omit<InputProps<AcceptableValue, ModelModifiers>, "modelValue" | "defaultValue"> | undefined;
  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; } | undefined;
}
```

### Slots

```ts
/**
 * Slots for the DateTimePicker component
 */
interface DateTimePickerSlots {
  /**
   * Replaces the trigger. Receives the open state and the formatted value.
   */
  default(): any;
  /**
   * Replaces the whole preset column.
   */
  presets(): any;
  /**
   * Replaces one preset button.
   */
  preset(): any;
  /**
   * Replaces the header of the time step.
   */
  time-header(): any;
}
```

### Emits

```ts
/**
 * Emitted events for the DateTimePicker component
 */
interface DateTimePickerEmits {
  update:open: (payload: [open: boolean]) => void;
  change: (payload: [value: DateValue | undefined]) => void;
  update:modelValue: (payload: [value: DateValue | undefined]) => void;
}
```

## Theme

```ts [app.config.ts]
export default defineAppConfig({
  b24ui: {
    dateTimePicker: {
      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'
      }
    }
  }
})
```

## Sitemap

See the full [sitemap](/b24ui/sitemap.md) for all pages.
