import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
<div>
<Label htmlFor='preview-starts-at'>Starts at</Label>
<DateTimeField dateFormat='dd.MM.yyyy' dateSeparator='.' separator=', ' timeFormat='HH:mm' defaultValue='2026-07-15T18:30'>
<DateTimeFieldInput id='preview-starts-at' placeholder='dd.mm.yyyy, hh:mm' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose start date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
</div>- Поддержка браузеров
- Недавно доступно, 2024
- Chrome 125
- Edge 125
- Firefox 128
- Safari 18
- Safari на iOS 18
- Размер в бандле
- Протестирован в
- 32 теста · 6 нояб.
Кратко
DateTimeField — составной компонент для совместного ввода даты и времени, который согласует ввод с клавиатуры, необязательный выбор в календаре и правила метки времени в одном поле. Это слой метки времени на основе поля для случаев, когда одно сохраняемое значение обозначает конкретный запланированный момент.
Импорт
import {
DateTimeField,
DateTimeFieldInput,
DateTimeFieldCalendar,
DateTimeFieldTriggerIconButton,
type DateTimeFieldProps,
} from '@vega-ui/react';Экспортируемые типы
DateTimeFieldPropsDateTimeFieldInputPropsDateTimeFieldCalendarPropsDateTimeFieldTriggerIconButtonProps
Минимальная композиция
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function MinimalComposition() {
return (
<DateTimeField dateFormat='dd.MM.yyyy' style={{ width: '100%', maxWidth: 320 }}>
<DateTimeFieldInput aria-label='Starts at' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose start date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
);
}Обязательные части
DateTimeField: владелец корневого состояния и формата метки времениDateTimeFieldInput: поле для ввода даты и времени с клавиатуры
Необязательные части
DateTimeFieldTriggerIconButton: триггер календаряDateTimeFieldCalendar: содержимое визуального пикера, обычно монтируется вPopover
Порядок композиции
DateTimeFieldDateTimeFieldInputPopoverDateTimeFieldTriggerIconButtonDateTimeFieldCalendar
Варианты
- Только поле ввода или поле ввода вместе с календарём
- Время с точностью до минут или до секунд
- Планирование с ограничениями через
min,max,disabledDatesиtimeStep
Пограничные случаи
- Интерпретация часового пояса остаётся за потребителем и должна быть задокументирована на уровне функции.
- Частичное редактирование метки времени — обычное дело, и его не стоит слишком рано считать итоговым значением.
- Поведение оверлеев нужно проверять внутри диалогов, выдвижных панелей (Drawer) и шторок (Sheet).
- Валидация даты и валидация времени должны сводиться к одной политике метки времени, а не к двум слабо связанным проверкам.
Типичные ошибки
- Передача в
min/maxобъектовDateили строк в формате отображения вместо ISO-строкYYYY-MM-DDTHH:mm[:ss]. - Валидация даты и времени как несвязанных правил.
- Неявный смысл часового пояса в сценариях планирования.
- Переопределения на уровне поля, из-за которых невалидное или отключённое состояние читается хуже.
Примеры
Базовый
Базовый: дата и время с календарём в поповере
Одно поле для даты и времени. Календарь выбирает день и сохраняет введённое время, а пока полного времени нет — подставляет 00:00. dateSeparator, separator и timeFormat здесь равны значениям по умолчанию и указаны явно; defaultValue — строка YYYY-MM-DDTHH:mm.
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];
export default function DateAndTimeWithACalendarPopover({ locale = 'en-US' }: { locale?: string }) {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 320 }}>
<Label htmlFor='starts-at'>Webinar starts at</Label>
<DateTimeField
locale={locale}
dateFormat='dd.MM.yyyy'
dateSeparator='.'
separator=', '
timeFormat='HH:mm'
defaultValue='2026-07-15T18:30'
>
<DateTimeFieldInput id='starts-at' placeholder='dd.mm.yyyy, hh:mm' aria-describedby='starts-at-hint' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose webinar date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
<HelperText id='starts-at-hint'>24-hour time. Picking a day keeps the typed time.</HelperText>
</div>
);
}Контролируемый/с состоянием
Контролируемый/с состоянием: окончание встречи следует за началом
Оба поля контролируемые. Начало — это min поля окончания, поэтому окончание, введённое раньше начала, ограничивается им, в том числе когда начало сдвигается уже после ввода окончания; длительность считается по двум ISO-значениям.
import { useState } from 'react';
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
Fieldset,
FieldsetLegend,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@vega-ui/react';
const describeDuration = (start: string, end: string) => {
if (!start || !end) return 'Choose the start and the end';
const minutes = (Date.parse(end) - Date.parse(start)) / 60_000;
const hours = Math.floor(minutes / 60);
const rest = minutes % 60;
if (hours === 0) return `${rest} min`;
if (rest === 0) return `${hours} h`;
return `${hours} h ${rest} min`;
};
export default function MeetingTimes() {
const [start, setStart] = useState('2026-07-15T10:00');
const [end, setEnd] = useState('2026-07-15T11:00');
return (
<Fieldset style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 320 }}>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-6)' }}>Meeting</FieldsetLegend>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='meeting-start'>Starts</Label>
<DateTimeField dateFormat='dd.MM.yyyy' timeStep={15} value={start} onChangeValue={setStart}>
<DateTimeFieldInput id='meeting-start' placeholder='dd.mm.yyyy, hh:mm' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose start date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
</div>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='meeting-end'>Ends</Label>
<DateTimeField dateFormat='dd.MM.yyyy' timeStep={15} min={start} value={end} onChangeValue={setEnd}>
<DateTimeFieldInput id='meeting-end' placeholder='dd.mm.yyyy, hh:mm' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose end date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
</div>
<Text size={2} aria-live='polite' style={{ color: 'var(--label-secondary)' }}>
Duration: {describeDuration(start, end)}
</Text>
</Fieldset>
);
}Контролируемый/с состоянием: быстрые напоминания
Кнопки задают контролируемое значение снаружи, а input показывает его в формате поля. Текущее время читается в обработчике клика, а не во время рендера.
import { useState } from 'react';
import {
Button,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@vega-ui/react';
const toISODateTime = (date: Date) => {
const pad = (part: number) => String(part).padStart(2, '0');
const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
return `${day}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
};
const inOneHour = () => {
const date = new Date();
date.setHours(date.getHours() + 1, 0, 0, 0);
return toISODateTime(date);
};
const tomorrowMorning = () => {
const date = new Date();
date.setDate(date.getDate() + 1);
date.setHours(9, 0, 0, 0);
return toISODateTime(date);
};
export default function SnoozePresets() {
const [remindAt, setRemindAt] = useState('');
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 320 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='remind-at'>Remind me at</Label>
<DateTimeField dateFormat='dd.MM.yyyy' value={remindAt} onChangeValue={setRemindAt}>
<DateTimeFieldInput id='remind-at' placeholder='dd.mm.yyyy, hh:mm' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose reminder date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
</div>
<div role='group' aria-label='Snooze presets' style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-3)' }}>
<Button size='xs' variant='secondary' appearance='ghost' onClick={() => setRemindAt(inOneHour())}>In 1 hour</Button>
<Button size='xs' variant='secondary' appearance='ghost' onClick={() => setRemindAt(tomorrowMorning())}>Tomorrow 09:00</Button>
<Button size='xs' variant='secondary' appearance='transparent' onClick={() => setRemindAt('')}>Clear</Button>
</div>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>
Value: {remindAt ? <code>{remindAt}</code> : 'no complete date and time'}
</Text>
</div>
);
}Форма/интеграция
Форма/интеграция: отправка локальных даты и времени
onChangeValue копирует значение в скрытый input, поэтому FormData содержит 2026-07-15T09:30, а не введённый текст. Это время по часам без смещения, поэтому обработчик отправки добавляет часовой пояс пользователя. required и pattern не пропускают пустое или неполное значение.
import { FormEvent, useState } from 'react';
import {
Button,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@vega-ui/react';
export default function SchedulePostForm() {
const [publishAt, setPublishAt] = useState('');
const [submitted, setSubmitted] = useState('');
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
data.set('timeZone', Intl.DateTimeFormat().resolvedOptions().timeZone);
setSubmitted(JSON.stringify(Object.fromEntries(data)));
};
return (
<form onSubmit={onSubmit} onReset={() => setSubmitted('')} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 320 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='publish-at'>Publish at</Label>
<DateTimeField dateFormat='dd.MM.yyyy' timeStep={15} min='2026-07-01T00:00' onChangeValue={setPublishAt}>
<DateTimeFieldInput
id='publish-at'
required
pattern='\d{2}\.\d{2}\.\d{4}, \d{2}:\d{2}'
placeholder='dd.mm.yyyy, hh:mm'
aria-describedby='publish-at-hint'
/>
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose publish date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
<input type='hidden' name='publishAt' value={publishAt} />
<HelperText id='publish-at-hint'>Your local time. The time zone is sent with the post.</HelperText>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
<Button type='reset' variant='secondary' appearance='ghost'>Reset</Button>
<Button type='submit'>Schedule</Button>
</div>
{submitted && <Text size={2} style={{ color: 'var(--label-secondary)', overflowWrap: 'anywhere' }}>Sent: <code>{submitted}</code></Text>}
</form>
);
}Раскладка/оверлей
Раскладка/оверлей: перенос внутри шторки
Поповер с календарём открывается поверх шторки, в которой находится поле. Шторка редактирует черновик, который при каждом открытии берётся из сохранённого значения, а форма с method='dialog' сохраняет его и закрывает шторку.
import { CSSProperties, useState } from 'react';
import { XIcon } from '@vega-ui/icons';
import {
Button,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
Heading,
Icon,
IconButton,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Sheet,
SheetClose,
SheetContent,
SheetTrigger,
Text,
} from '@vega-ui/react';
export default function ReschedulingInsideASheet() {
const [draft, setDraft] = useState('2026-07-15T14:00');
const [appointment, setAppointment] = useState('2026-07-15T14:00');
const onChangeOpen = (open: boolean) => {
if (open) setDraft(appointment);
};
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'center', width: '100%' }}>
<Text size={2} style={{ color: 'var(--label-secondary)', textAlign: 'center' }}>Appointment: <code>{appointment}</code></Text>
<Sheet onChangeOpen={onChangeOpen}>
<SheetTrigger asChild>
<Button>Reschedule</Button>
</SheetTrigger>
<SheetContent aria-labelledby='reschedule-title'>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
<Heading id='reschedule-title' as='h2' size={4} fontWeight={600}>Reschedule appointment</Heading>
<SheetClose asChild>
<IconButton
aria-label='Close'
variant='secondary'
appearance='ghost'
style={{ flexShrink: 0, '--icon-button-size': '24px', '--icon-button-br-ratio': 0.5 } as CSSProperties}
>
<Icon size='xs'><XIcon strokeWidth={2.5} /></Icon>
</IconButton>
</SheetClose>
</div>
<form
method='dialog'
onSubmit={() => setAppointment(draft)}
style={{ display: 'grid', gap: 'var(--spacing-12)', marginBlockStart: 'var(--spacing-8)' }}
>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='new-time'>New time</Label>
<DateTimeField dateFormat='dd.MM.yyyy' timeStep={15} value={draft} onChangeValue={setDraft}>
<DateTimeFieldInput id='new-time' required pattern='\d{2}\.\d{2}\.\d{4}, \d{2}:\d{2}' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose new date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
<Button type='submit'>Save</Button>
</div>
</form>
</SheetContent>
</Sheet>
</div>
);
}Ошибка
Ошибка: обязательная и неполная временная метка
Нативные required и pattern вызывают invalid при отправке, пока поле пустое или дата либо время неполные. Обработчик выбирает сообщение по validity, error и aria-invalid помечают поле, а ввод сбрасывает ошибку.
import { FormEvent, useState } from 'react';
import {
Button,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
const HINT = 'Date and 24-hour time, for example 15.07.2026, 09:30.';
export default function MaintenanceWindowForm() {
const [error, setError] = useState('');
const invalid = error !== '';
const onInvalid = (event: FormEvent<HTMLInputElement>) => {
if (event.currentTarget.validity.valueMissing) {
setError('Enter when the maintenance starts.');
return;
}
setError('The date or the time is incomplete.');
};
return (
<form onSubmit={(event) => event.preventDefault()} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 320 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='maintenance-start'>Maintenance starts</Label>
<DateTimeField dateFormat='dd.MM.yyyy' error={invalid}>
<DateTimeFieldInput
id='maintenance-start'
name='maintenanceStart'
required
pattern='\d{2}\.\d{2}\.\d{4}, \d{2}:\d{2}'
placeholder='dd.mm.yyyy, hh:mm'
aria-invalid={invalid}
aria-describedby='maintenance-start-hint'
onInvalid={onInvalid}
onInput={() => setError('')}
/>
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose maintenance date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
<HelperText id='maintenance-start-hint' error={invalid}>{invalid ? error : HINT}</HelperText>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
<Button type='submit'>Save</Button>
</div>
</form>
);
}Отключённый
Отключённый: зафиксированное время выполнения
disabled на корне отключает input и триггер календаря, а вспомогательный текст объясняет причину.
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function LockedExecutionTime() {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 320 }}>
<Label htmlFor='executed-at'>Executed at</Label>
<DateTimeField dateFormat='dd.MM.yyyy' disabled defaultValue='2026-07-15T09:30'>
<DateTimeFieldInput id='executed-at' aria-describedby='executed-at-hint' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose execution date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
<HelperText id='executed-at-hint'>The job has already run and cannot be rescheduled.</HelperText>
</div>
);
}Пограничный случай
Пограничный случай: рабочие часы
min и max включают время: полное значение вне их заменяется ближайшей границей, поэтому 01.07.2026, 08:30 становится 01.07.2026, 09:00. Часы и минуты ограничиваются своим диапазоном, поэтому 25:30 становится 23:30.
import { useState } from 'react';
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@vega-ui/react';
export default function BusinessHoursWindow() {
const [slot, setSlot] = useState('');
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 320 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='delivery-slot'>Delivery slot</Label>
<DateTimeField
dateFormat='dd.MM.yyyy'
min='2026-07-01T09:00'
max='2026-07-31T18:00'
value={slot}
onChangeValue={setSlot}
>
<DateTimeFieldInput id='delivery-slot' placeholder='dd.mm.yyyy, hh:mm' aria-describedby='delivery-slot-hint' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose delivery day' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
<HelperText id='delivery-slot-hint'>From 01.07.2026, 09:00 to 31.07.2026, 18:00. Try 01.07.2026, 08:30 or 25:30.</HelperText>
</div>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>
Value: {slot ? <code>{slot}</code> : 'no complete date and time'}
</Text>
</div>
);
}Пограничный случай: шаг в четверть часа
timeStep задаёт, на сколько ArrowUp и ArrowDown сдвигают минуты под кареткой, переходя от 45 к 00 без смены часа. На часах стрелки по-прежнему двигают на единицу, как нативный datetime-local со step. Введённые минуты остаются как есть.
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function QuarterHourSteps() {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 320 }}>
<Label htmlFor='pickup-time'>Pickup</Label>
<DateTimeField dateFormat='dd.MM.yyyy' timeStep={15} defaultValue='2026-07-15T12:45'>
<DateTimeFieldInput id='pickup-time' aria-describedby='pickup-time-hint' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose pickup date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
<HelperText id='pickup-time-hint'>Put the caret in the minutes and press ↑ or ↓ to move by 15 minutes.</HelperText>
</div>
);
}Пограничный случай: точность до секунд
timeFormat='HH:mm:ss' добавляет сегмент секунд; значение тогда тоже содержит секунды, и min и max могут их включать.
import { useState } from 'react';
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@vega-ui/react';
export default function SecondsPrecision() {
const [loggedAt, setLoggedAt] = useState('2026-07-01T23:59:58');
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 320 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='logged-at'>Log entry time</Label>
<DateTimeField
dateFormat='dd.MM.yyyy'
timeFormat='HH:mm:ss'
min='2026-07-01T00:00:00'
value={loggedAt}
onChangeValue={setLoggedAt}
>
<DateTimeFieldInput id='logged-at' placeholder='dd.mm.yyyy, hh:mm:ss' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose log date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
</div>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>
Value: {loggedAt ? <code>{loggedAt}</code> : 'no complete date and time'}
</Text>
</div>
);
}Пограничный случай: только будние дни
disabledDates принимает ISO-дни или функцию и действует только на календарь. Введённый выходной принимается, поэтому обработчик отправки проверяет то же правило isWeekend по части с датой и показывает ошибку.
import { FormEvent, useState } from 'react';
import {
Button,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
const isWeekend = (iso: string) => {
const weekday = new Date(`${iso.slice(0, 10)}T00:00:00Z`).getUTCDay();
return weekday === 0 || weekday === 6;
};
export default function WeekdaysOnly() {
const [meeting, setMeeting] = useState('');
const [error, setError] = useState('');
const invalid = error !== '';
const onChangeValue = (value: string) => {
setMeeting(value);
setError('');
};
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (meeting && isWeekend(meeting)) {
setError('The office is closed on weekends. Choose a weekday.');
}
};
return (
<form onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 320 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='office-meeting'>Office meeting</Label>
<DateTimeField
dateFormat='dd.MM.yyyy'
disabledDates={isWeekend}
error={invalid}
value={meeting}
onChangeValue={onChangeValue}
>
<DateTimeFieldInput
id='office-meeting'
required
placeholder='dd.mm.yyyy, hh:mm'
aria-invalid={invalid}
aria-describedby='office-meeting-hint'
/>
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose meeting date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
<HelperText id='office-meeting-hint' error={invalid}>{invalid ? error : 'Monday to Friday.'}</HelperText>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
<Button type='submit'>Book room</Button>
</div>
</form>
);
}Анатомия
Обзор
DateTimeField — составной компонент, который под одним корнем согласует ввод даты и времени с клавиатуры, необязательный выбор в календаре и общие правила для метки времени.
Обязательные части
DateTimeField
Обязательная. Отвечает за разбор даты, разбор времени, правила шага и общее состояние метки времени.
DateTimeFieldInput
Обязательная. Поле для ввода даты и времени с клавиатуры.
Необязательные части
DateTimeFieldTriggerIconButton
Необязательный триггер для открытия календаря в оверлее.
DateTimeFieldCalendar
Необязательный визуальный выбор даты, синхронизированный со значением поля.
Его дочерние элементы — части Calendar: CalendarHeader с CalendarPrevButton, CalendarViewButtonGroup (CalendarViewButton view='month' и view='year') и CalendarNextButton, затем CalendarContent с CalendarDays, CalendarMonths и CalendarYears. Календарь получает locale и calendar от DateTimeField.
Порядок композиции
DateTimeFieldDateTimeFieldInputPopoverDateTimeFieldTriggerIconButtonDateTimeFieldCalendarCalendarHeaderиCalendarContentс представлениями календаря
Корректные паттерны композиции
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateTimeField,
DateTimeFieldCalendar,
DateTimeFieldInput,
DateTimeFieldTriggerIconButton,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function MinimalComposition() {
return (
<DateTimeField dateFormat='dd.MM.yyyy' style={{ width: '100%', maxWidth: 320 }}>
<DateTimeFieldInput aria-label='Starts at' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateTimeFieldTriggerIconButton aria-label='Choose start date' />
</PopoverTrigger>
<PopoverContent>
<DateTimeFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateTimeFieldCalendar>
</PopoverContent>
</Popover>
</DateTimeField>
);
}Некорректные паттерны композиции
Input вне DateTimeField
Это ломает общий разбор даты и времени и синхронизацию.
Сценарий с меткой времени без явных правил часового пояса
Поле обрабатывает ввод, а не продуктовую политику часовых поясов.
Валидация даты и времени как несвязанных правил
Итоговое значение в продукте — одна метка времени, а не два независимых поля.