import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
<div>
<Label htmlFor='preview-due-date'>Due date</Label>
<DateField format='dd.MM.yyyy' separator='.' defaultValue='2026-11-03'>
<DateFieldInput id='preview-due-date' placeholder='dd.mm.yyyy' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose due date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
</div>- Поддержка браузеров
- Недавно доступно, 2024
- Chrome 125
- Edge 125
- Firefox 128
- Safari 18
- Safari на iOS 18
- Размер в бандле
- Протестирован в
- 38 тестов · 6 нояб.
Кратко
DateField — составной компонент для ввода одной даты, который объединяет ввод с клавиатуры, необязательный выбор в календаре и ограничения на уровне поля в одном элементе управления. Это слой ввода даты, где главное — поле, а календарь служит необязательным помощником для выбора, а не основной абстракцией.
Импорт
import {
DateField,
DateFieldInput,
DateFieldCalendar,
DateFieldTriggerIconButton,
type DateFieldProps,
} from '@vega-ui/react';Экспортируемые типы
DateFieldPropsDateFieldInputPropsDateFieldCalendarPropsDateFieldTriggerIconButtonProps
Минимальная композиция
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function MinimalComposition() {
return (
<DateField format='dd.MM.yyyy' separator='.'>
<DateFieldInput aria-label='Due date' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose due date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
);
}Обязательные части
DateField: владелец корневого состояния и форматированияDateFieldInput: поверхность для ввода даты
Необязательные части
DateFieldTriggerIconButton: триггер календаряDateFieldCalendar: содержимое визуального выбора даты, обычно монтируется вPopover
Порядок композиции
DateFieldDateFieldInputPopoverDateFieldTriggerIconButtonDateFieldCalendar
Варианты
- Только input или input вместе с календарём
- Выбор через триггер или только ввод с клавиатуры
- Ограниченное состояние через
min,maxиdisabledDates
Пограничные случаи
- Правила локали, часового пояса и сериализации по-прежнему остаются на стороне потребителя.
- Частично введённые значения во время редактирования — обычное дело, и их не следует считать итоговыми.
- Поведение оверлея проверяйте внутри диалогов, выдвижных панелей и прокручиваемых контейнеров.
- Одно и то же бизнес-правило должно управлять и валидацией ввода, и блокировкой в календаре; они не должны расходиться в две разные политики.
Частые ошибки
- Передача в
min/maxобъектовDateили строк в формате отображения вместо ISO-строкYYYY-MM-DD. - Расчёт на то, что поле само решит вопрос с политикой часовых поясов.
- Иконка-кнопка триггера без подписи в сценариях с вспомогательными технологиями.
- Восприятие
min,maxиdisabledDatesкак визуальных подсказок, а не правил валидации.
Примеры
Базовый
Базовый: ввод даты с календарём в поповере
Поле с подписью, в которое можно ввести дату или выбрать её в календаре в поповере. defaultValue на корне — ISO-день YYYY-MM-DD; input показывает его в формате format, а вспомогательный текст подсказывает, в каком формате вводить.
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];
export default function TypedDateWithACalendarPopover({ locale = 'en-US' }: { locale?: string }) {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 280 }}>
<Label htmlFor='due-date'>Payment due</Label>
<DateField locale={locale} format='dd.MM.yyyy' separator='.' defaultValue='2026-03-31'>
<DateFieldInput id='due-date' placeholder='dd.mm.yyyy' aria-describedby='due-date-hint' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose due date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
<HelperText id='due-date-hint'>Type the date as dd.mm.yyyy or pick it in the calendar.</HelperText>
</div>
);
}Контролируемый/с состоянием
Контролируемый/с состоянием: быстрые даты
value и onChangeValue на корне держат ISO-день в состоянии родителя. Кнопки задают его снаружи, а input показывает его в формате format; пока введённый текст неполный, значение равно ''.
import { useState } from 'react';
import {
Button,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@vega-ui/react';
const toISODate = (date: Date) => {
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${date.getFullYear()}-${month}-${day}`;
};
const daysFromToday = (days: number) => {
const date = new Date();
date.setDate(date.getDate() + days);
return toISODate(date);
};
export default function PresetDates() {
const [publishOn, setPublishOn] = useState('');
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 360 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='publish-on'>Publish on</Label>
<DateField format='dd.MM.yyyy' separator='.' value={publishOn} onChangeValue={setPublishOn}>
<DateFieldInput id='publish-on' placeholder='dd.mm.yyyy' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose publish date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
</div>
<div role='group' aria-label='Quick dates' style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-3)' }}>
<Button size='xs' variant='secondary' appearance='ghost' onClick={() => setPublishOn(daysFromToday(0))}>Today</Button>
<Button size='xs' variant='secondary' appearance='ghost' onClick={() => setPublishOn(daysFromToday(1))}>Tomorrow</Button>
<Button size='xs' variant='secondary' appearance='ghost' onClick={() => setPublishOn(daysFromToday(7))}>In a week</Button>
<Button size='xs' variant='secondary' appearance='transparent' onClick={() => setPublishOn('')}>Clear</Button>
</div>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>
Value: {publishOn ? <code>{publishOn}</code> : 'no complete date'}
</Text>
</div>
);
}Контролируемый/с состоянием: выезд не раньше заезда
День заезда задаёт min поля выезда. Когда заезд сдвигается за уже введённый выезд, выезд ограничивается новым min, а onChangeValue сообщает ограниченный день, поэтому число ночей остаётся верным.
import { useState } from 'react';
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
Fieldset,
FieldsetLegend,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@vega-ui/react';
const addDays = (iso: string, days: number) => {
const date = new Date(`${iso}T00:00:00Z`);
date.setUTCDate(date.getUTCDate() + days);
return date.toISOString().slice(0, 10);
};
const countNights = (checkIn: string, checkOut: string) => {
return (Date.parse(checkOut) - Date.parse(checkIn)) / 86_400_000;
};
export default function StayDates() {
const [checkIn, setCheckIn] = useState('');
const [checkOut, setCheckOut] = useState('');
const checkOutMin = checkIn ? addDays(checkIn, 1) : undefined;
const nights = checkIn && checkOut ? countNights(checkIn, checkOut) : 0;
return (
<Fieldset style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 280 }}>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-6)' }}>Stay</FieldsetLegend>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='check-in'>Check-in</Label>
<DateField format='dd.MM.yyyy' separator='.' value={checkIn} onChangeValue={setCheckIn}>
<DateFieldInput id='check-in' placeholder='dd.mm.yyyy' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose check-in date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
</div>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='check-out'>Check-out</Label>
<DateField format='dd.MM.yyyy' separator='.' min={checkOutMin} value={checkOut} onChangeValue={setCheckOut}>
<DateFieldInput id='check-out' placeholder='dd.mm.yyyy' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose check-out date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
</div>
<Text size={2} aria-live='polite' style={{ color: 'var(--label-secondary)' }}>
{nights > 0 ? `${nights} ${nights === 1 ? 'night' : 'nights'}` : 'Choose both dates'}
</Text>
</Fieldset>
);
}Форма/интеграция
Форма/интеграция: отправка ISO-даты
DateFieldInput — нативный <input>: с name он отправил бы введённый текст, например 15.01.2026. Здесь onChangeValue копирует ISO-день в скрытый input, и FormData получает 2026-01-15. required и pattern не пропускают пустую или неполную дату, а кнопка сброса возвращает defaultValue.
import { FormEvent, useState } from 'react';
import {
Button,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@vega-ui/react';
const DEFAULT_INVOICE_DATE = '2026-01-15';
export default function InvoiceForm() {
const [invoiceDate, setInvoiceDate] = useState(DEFAULT_INVOICE_DATE);
const [submitted, setSubmitted] = useState('');
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
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='invoice-date'>Invoice date</Label>
<DateField format='dd.MM.yyyy' separator='.' defaultValue={DEFAULT_INVOICE_DATE} onChangeValue={setInvoiceDate}>
<DateFieldInput id='invoice-date' required pattern='\d{2}\.\d{2}\.\d{4}' aria-describedby='invoice-date-hint' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose invoice date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
<input type='hidden' name='invoiceDate' value={invoiceDate} />
<HelperText id='invoice-date-hint'>Used as the accounting reference date.</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'>Save</Button>
</div>
{submitted && <Text size={2} style={{ color: 'var(--label-secondary)', overflowWrap: 'anywhere' }}>Sent: <code>{submitted}</code></Text>}
</form>
);
}Раскладка/оверлей
Раскладка/оверлей: поле даты внутри диалога
Поповер с календарём открывается поверх диалога, в котором находится поле. Форма использует method='dialog', поэтому валидная отправка закрывает диалог.
import { CSSProperties } 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,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function DateFieldInsideADialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Schedule review</Button>
</DialogTrigger>
<DialogContent aria-labelledby='schedule-review-title' style={{ minWidth: 320 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
<Heading id='schedule-review-title' as='h2' size={4} fontWeight={600}>Schedule review</Heading>
<DialogClose 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>
</DialogClose>
</div>
<form method='dialog' style={{ display: 'grid', gap: 'var(--spacing-12)', marginBlockStart: 'var(--spacing-8)' }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='review-date'>Review date</Label>
<DateField format='dd.MM.yyyy' separator='.' min='2026-01-01'>
<DateFieldInput id='review-date' name='reviewDate' required placeholder='dd.mm.yyyy' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose review date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
<Button type='submit'>Schedule</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}Раскладка/оверлей: закрыть календарь после выбора
По умолчанию поповер остаётся открытым после выбора, чтобы его можно было поправить. Чтобы закрывать его при выборе, сделайте open у Popover контролируемым и закрывайте его из onChangeValue у DateFieldCalendar; переход по месяцам его не вызывает.
import { useState } from 'react';
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function CloseCalendarAfterPick() {
const [open, setOpen] = useState(false);
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 280 }}>
<Label htmlFor='delivery-date'>Delivery date</Label>
<DateField format='dd.MM.yyyy' separator='.'>
<DateFieldInput id='delivery-date' placeholder='dd.mm.yyyy' />
<Popover placement='bottom-end' open={open} onChangeOpen={setOpen}>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose delivery date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar onChangeValue={() => setOpen(false)}>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
</div>
);
}Ошибка
Ошибка: обязательные и неполные даты
required и pattern — нативные ограничения input: при отправке срабатывает 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,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
const HINT = 'Enter the full date as dd.mm.yyyy.';
export default function RenewalForm() {
const [error, setError] = useState('');
const invalid = error !== '';
const onInvalid = (event: FormEvent<HTMLInputElement>) => {
if (event.currentTarget.validity.valueMissing) {
setError('Enter the renewal date.');
return;
}
setError('The date is incomplete. Use dd.mm.yyyy.');
};
return (
<form onSubmit={(event) => event.preventDefault()} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 280 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='renewal-date'>Renewal date</Label>
<DateField format='dd.MM.yyyy' separator='.' error={invalid}>
<DateFieldInput
id='renewal-date'
name='renewalDate'
required
pattern='\d{2}\.\d{2}\.\d{4}'
placeholder='dd.mm.yyyy'
aria-invalid={invalid}
aria-describedby='renewal-date-hint'
onInvalid={onInvalid}
onInput={() => setError('')}
/>
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose renewal date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
<HelperText id='renewal-date-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,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function LockedContractDate() {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 280 }}>
<Label htmlFor='signed-on'>Signed on</Label>
<DateField format='dd.MM.yyyy' separator='.' disabled defaultValue='2026-01-15'>
<DateFieldInput id='signed-on' aria-describedby='signed-on-hint' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose signing date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
<HelperText id='signed-on-hint'>The contract is signed. Create an amendment to change the date.</HelperText>
</div>
);
}Отключённый: отключение через fieldset
Нативный <fieldset disabled> отключает input и кнопку-триггер внутри себя, поэтому календарь тоже не открыть. Чекбокс переключает всю группу, не передавая disabled в поле.
import { useState } from 'react';
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
Checkbox,
CheckboxCheckedIcon,
CheckboxIndeterminateIcon,
CheckboxIndicator,
CheckboxInput,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
Fieldset,
FieldsetLegend,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function DisabledByFieldset() {
const [asap, setAsap] = useState(true);
return (
<div style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 300 }}>
<Checkbox style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
<CheckboxInput checked={asap} onChange={(event) => setAsap(event.currentTarget.checked)} />
<CheckboxIndicator>
<CheckboxCheckedIcon />
<CheckboxIndeterminateIcon />
</CheckboxIndicator>
Ship as soon as possible
</Checkbox>
<Fieldset disabled={asap} style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-6)' }}>Scheduled shipping</FieldsetLegend>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='ship-on'>Ship on</Label>
<DateField format='dd.MM.yyyy' separator='.'>
<DateFieldInput id='ship-on' name='shipOn' placeholder='dd.mm.yyyy' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose shipping date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
</div>
</Fieldset>
</div>
);
}Отключённый: дата только для чтения
readOnly на DateFieldInput оставляет значение фокусируемым, выделяемым и отправляемым с формой. У поля нет своего readOnly, а DateFieldCalendar пишет в input, даже если он только для чтения, поэтому триггер календаря здесь не нужен.
import { DateField, DateFieldInput, HelperText, Label } from '@vega-ui/react';
export default function ReadOnlyDate() {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 280 }}>
<Label htmlFor='account-created'>Account created</Label>
<DateField format='dd.MM.yyyy' separator='.' defaultValue='2024-09-02'>
<DateFieldInput id='account-created' name='accountCreated' readOnly aria-describedby='account-created-hint' />
</DateField>
<HelperText id='account-created-hint'>Set by the system. The value is still submitted with the form.</HelperText>
</div>
);
}Пограничный случай
Пограничный случай: введённые даты ограничиваются
Введённый день после конца месяца ограничивается длиной месяца, поэтому 31.02.2026 становится 28.02.2026. Полная дата вне min и max заменяется ближайшей границей, поэтому 05.01.2026 становится 10.01.2026. Календарь отключает те же дни.
import { useState } from 'react';
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@vega-ui/react';
export default function TypedDatesAreClamped() {
const [startDate, setStartDate] = useState('');
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%', maxWidth: 280 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='campaign-start'>Campaign start</Label>
<DateField
format='dd.MM.yyyy'
separator='.'
min='2026-01-10'
max='2026-12-31'
value={startDate}
onChangeValue={setStartDate}
>
<DateFieldInput id='campaign-start' placeholder='dd.mm.yyyy' aria-describedby='campaign-start-hint' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose campaign start' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
<HelperText id='campaign-start-hint'>Between 10.01.2026 and 31.12.2026. Try 31.02.2026 or 05.01.2026.</HelperText>
</div>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>
Value: {startDate ? <code>{startDate}</code> : 'no complete date'}
</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,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
const isWeekend = (iso: string) => {
const weekday = new Date(`${iso}T00:00:00Z`).getUTCDay();
return weekday === 0 || weekday === 6;
};
export default function WeekendsUnavailable() {
const [deliveryDate, setDeliveryDate] = useState('');
const [error, setError] = useState('');
const invalid = error !== '';
const onChangeValue = (date: string) => {
setDeliveryDate(date);
setError('');
};
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (deliveryDate && isWeekend(deliveryDate)) {
setError('We do not deliver on weekends. Choose a weekday.');
}
};
return (
<form onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 300 }}>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='delivery-date'>Delivery date</Label>
<DateField
format='dd.MM.yyyy'
separator='.'
disabledDates={isWeekend}
error={invalid}
value={deliveryDate}
onChangeValue={onChangeValue}
>
<DateFieldInput
id='delivery-date'
required
placeholder='dd.mm.yyyy'
aria-invalid={invalid}
aria-describedby='delivery-date-hint'
/>
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose delivery date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
<HelperText id='delivery-date-hint' error={invalid}>{invalid ? error : 'Monday to Friday only.'}</HelperText>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
<Button type='submit'>Book delivery</Button>
</div>
</form>
);
}Пограничный случай: всплывающий календарь в другой локали
locale задаёт названия месяцев и дней недели и первый день недели в календаре. Формат ввода по-прежнему берётся из format, а подписи триггера и кнопок месяца переводите сами.
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
HelperText,
Label,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function CalendarPopupInAnotherLocale() {
return (
<div lang='de-DE' style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 280 }}>
<Label htmlFor='liefertermin'>Liefertermin</Label>
<DateField format='dd.MM.yyyy' separator='.' locale='de-DE'>
<DateFieldInput id='liefertermin' placeholder='TT.MM.JJJJ' aria-describedby='liefertermin-hint' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Datum auswählen' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<CalendarHeader>
<CalendarPrevButton aria-label='Vorheriger Monat' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Nächster Monat' />
</CalendarHeader>
<CalendarContent>
<CalendarDays>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
<HelperText id='liefertermin-hint'>Die Woche beginnt im Kalender am Montag.</HelperText>
</div>
);
}Анатомия
Обзор
DateField — составной компонент, который координирует ввод даты с клавиатуры, необязательный выбор в календаре и общее состояние поля под одним корнем.
Обязательные части
DateField
Обязательна. Отвечает за разбор и форматирование даты, правила отключённых дат и общее состояние поля.
DateFieldInput
Обязательна. Предоставляет поверхность для ввода даты.
Необязательные части
DateFieldTriggerIconButton
Необязательный триггер для открытия оверлея с календарём.
DateFieldCalendar
Необязательный визуальный выбор даты; обычно его монтируют внутри Popover.
Его дочерние элементы — части Calendar: CalendarHeader с CalendarPrevButton, CalendarViewButtonGroup (CalendarViewButton view='month' и view='year') и CalendarNextButton, затем CalendarContent с CalendarDays, CalendarMonths и CalendarYears. Календарь берёт locale и calendar из DateField.
Порядок композиции
DateFieldDateFieldInputPopoverDateFieldTriggerIconButtonDateFieldCalendarCalendarHeaderиCalendarContentс представлениями календаря
Допустимые паттерны композиции
import {
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
DateField,
DateFieldCalendar,
DateFieldInput,
DateFieldTriggerIconButton,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
export default function MinimalComposition() {
return (
<DateField format='dd.MM.yyyy' separator='.'>
<DateFieldInput aria-label='Due date' />
<Popover placement='bottom-end'>
<PopoverTrigger asChild>
<DateFieldTriggerIconButton aria-label='Choose due date' />
</PopoverTrigger>
<PopoverContent>
<DateFieldCalendar>
<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>
</DateFieldCalendar>
</PopoverContent>
</Popover>
</DateField>
);
}Недопустимые паттерны композиции
DateFieldInput вне DateField
Это ломает общее форматирование и синхронизацию значения.
Триггер календаря без реального оверлея календаря
Кнопка обещает выбор даты, которого нет.
Ограничения календаря как чисто визуальные
min, max и disabledDates должны соответствовать реальным правилам валидации.