import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
} from '@vega-ui/react';
<Calendar locale='en-US' defaultValue='2026-11-03'>
<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>
</Calendar>- Поддержка браузеров
- Недавно доступно, 2024
- Chrome 125
- Edge 125
- Firefox 128
- Safari 18
- Safari на iOS 18
- Размер в бандле
- Протестирован в
- 144 теста · 6 нояб.
- Relative colors
- Oklab and OkLCh
- Scroll snap
- scrollend — в коде есть запасной вариант, в минимум не входит
- scrollbar-width — есть запасной вариант с префиксом -webkit-, в минимум не входит
- inert
- Intl.Locale
- Intl.Locale info — в коде есть запасной вариант, в минимум не входит
- Resize observer
- aspect-ratio
- Logical properties
- :dir() — от неё зависят только макеты справа налево
- :focus-visible
- :where()
- contain
- scroll-behavior
Кратко
Calendar — продвинутый интерактивный составной компонент для согласованного выбора даты в видах дней, месяцев и лет. Это слой оркестрации для потребителей поверх низкоуровневого семейства компонентов выбора.
Импорты
import {
Calendar,
type CalendarProps,
type CalendarView,
type CalendarSelection,
type CalendarDatesDisabled,
} from '@vega-ui/react';Экспортируемые типы
CalendarPropsCalendarViewCalendarSelectionCalendarDatesDisabled
Минимальная композиция
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
} from '@vega-ui/react';
export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];
export default function MinimalComposition({ locale = 'en-US' }: { locale?: string }) {
return (
<Calendar locale={locale}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Days'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
);
}Обязательные части
Calendar: состояние корня и модель выбора даты
Необязательные части
CalendarHeader,CalendarPrevButton,CalendarNextButton: элементы навигации; «назад» и «вперёд» следуют текущему видуCalendarViewButtonGroup,CalendarViewButton: переключатели видов месяцев и летCalendarMonthLabel,CalendarYearLabel: подписи видимого месяца и годаCalendarContent: обёртка для видовCalendarDays,CalendarMonths,CalendarYears: виды дней, месяцев и летCalendarDay,CalendarMonth,CalendarYear: шаблоны ячеек видов, сuseCalendarDayContext,useCalendarMonthContext,useCalendarYearContext
Порядок композиции
Типичная композиция:
CalendarCalendarHeaderсCalendarPrevButton,CalendarViewButtonGroup,CalendarNextButtonCalendarContentCalendarDays,CalendarMonths,CalendarYears; видим только текущийview
Варианты
- Выбор:
single,multiple,range - Переключение видов: дни, месяцы, годы
- Плотность:
compactи варианты размеров - Глубина композиции: стандартные виды или пользовательские шаблоны ячеек
- Локаль и календарная система: пропсы корня
localeиcalendar, по умолчанию — локаль среды выполнения и григорианский календарь; см. Даты и календарные системы
Пограничные случаи
- Режим выбора меняет форму значения.
- Ограничения отключённых дат могут задаваться списком значений или логикой предиката.
- Пользовательские шаблоны ячеек нужно перепроверять на клавиатурную навигацию, дни соседних месяцев и смену видимого периода.
value,defaultValue,min,max,monthиdisabledDates— ISO-строкиYYYY-MM-DDв любой календарной системе; только сетка и подписи следуютcalendar.- Негригорианские календари создаются через
createCalendarSystem; см. Даты и календарные системы.
Частые ошибки
- Забывать, что
selectionменяет ожидаемую модель значения. - Передавать объекты
Date, временные метки или строки отображаемого календаря вместо ISO-строкYYYY-MM-DD. - Брать номер дня из ISO
dateячейки вместоlabelилиfields.day; в негригорианских календарях это неверно. - Комбинировать
min,maxиdisabledDates, не проверив итоговый UX.
Примеры
Базовый
Базовый: одна дата в локали
Полная композиция: заголовок с навигацией и переключателями видов, затем виды дней, месяцев и лет. locale задаёт названия месяцев и дней недели и первый день недели; defaultValue — ISO-день, который также открывает свой месяц. У кнопок навигации и видов нет встроенных имён, поэтому им переданы aria-label на языке календаря.
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
} from '@vega-ui/react';
export default function SingleDateInALocale() {
return (
<Calendar locale='de-DE' defaultValue='2026-03-15'>
<CalendarHeader>
<CalendarPrevButton aria-label='Zurück' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'>
<CalendarMonthLabel />
</CalendarViewButton>
<CalendarViewButton view='year'>
<CalendarYearLabel />
</CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Weiter' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Tage'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths aria-label='Monate'>
<CalendarMonth />
</CalendarMonths>
<CalendarYears aria-label='Jahre'>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
);
}Контролируемый/с состоянием
Контролируемый/с состоянием: значение и видимый месяц
И value, и month контролируемые, поэтому кнопка «Today» за один шаг выбирает сегодняшний день и прокручивает к нему. Навигация только вызывает onChangeMonth, за которым следит подпись под календарём; значение меняется, лишь когда выбран день.
import {
Button,
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
Text,
} from '@vega-ui/react';
import { useState } from 'react';
const dateFormat = new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' });
function getToday() {
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${now.getFullYear()}-${month}-${day}`;
}
export default function ValueAndVisibleMonth() {
const [value, setValue] = useState('');
const [month, setMonth] = useState(getToday);
const showToday = () => {
const today = getToday();
setValue(today);
setMonth(today);
};
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', justifyItems: 'start', width: '100%' }}>
<Calendar locale='en-US' value={value} onChangeValue={setValue} month={month} onChangeMonth={setMonth}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Delivery date'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
<div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
<Text aria-live='polite'>
Delivery: {value ? dateFormat.format(new Date(value)) : 'not scheduled'}
</Text>
<Text style={{ color: 'var(--label-secondary)' }}>
Showing {monthFormat.format(new Date(month))}
</Text>
</div>
<div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
<Button size='sm' variant='secondary' appearance='ghost' onClick={showToday}>Today</Button>
<Button size='sm' variant='secondary' appearance='ghost' onClick={() => setValue('')} disabled={!value}>Clear</Button>
</div>
</div>
);
}Контролируемый/с состоянием: диапазон со сводкой
В режиме диапазона значение — кортеж [start, end]; '' обозначает ещё не выбранную границу. Границы всегда упорядочены, start раньше end, какая бы ни была выбрана первой, поэтому сводка считает ночи без сортировки.
import {
Button,
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
Text,
} from '@vega-ui/react';
import { useState } from 'react';
const DAY = 86_400_000;
export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];
function getSummary(locale: string, start: string, end: string) {
const dateFormat = new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric', timeZone: 'UTC' });
if (!start) return 'Pick a check-in date';
if (!end) return `Check-in ${dateFormat.format(new Date(start))}, pick a check-out date`;
const nights = (Date.parse(end) - Date.parse(start)) / DAY;
const range = dateFormat.formatRange(new Date(start), new Date(end));
if (nights === 1) return `${range}: 1 night`;
return `${range}: ${nights} nights`;
}
export default function RangeWithASummary({ locale = 'en-US' }: { locale?: string }) {
const [[start, end], setRange] = useState<[string, string]>(['', '']);
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
<Calendar locale={locale} selection='range' value={[start, end]} onChangeValue={setRange}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Stay dates'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
<Text aria-live='polite' style={{ fontVariantNumeric: 'tabular-nums' }}>{getSummary(locale, start, end)}</Text>
<Button size='sm' variant='secondary' appearance='ghost' onClick={() => setRange(['', ''])} disabled={!start}>
Clear dates
</Button>
</div>
);
}Контролируемый/с состоянием: несколько дат с ограничением
selection='multiple' делает значение массивом ISO-дней в порядке выбора. Контролируемое значение может отклонить изменение: здесь onChangeValue игнорирует шестой день, и календарь продолжает показывать пять.
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
Text,
} from '@vega-ui/react';
import { useState } from 'react';
const MAX_DAYS = 5;
const dateFormat = new Intl.DateTimeFormat('en-US', { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' });
export default function MultipleDates() {
const [days, setDays] = useState<string[]>(['2026-11-04', '2026-11-11']);
const change = (next: string[]) => {
if (next.length > MAX_DAYS) return;
setDays(next);
};
const sorted = [...days].sort();
const isFull = days.length === MAX_DAYS;
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
<Calendar locale='en-US' selection='multiple' value={days} onChangeValue={change}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Office days'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Text aria-live='polite' fontWeight={500} style={{ fontVariantNumeric: 'tabular-nums' }}>
{days.length} of {MAX_DAYS} office days{isFull ? ', unselect a day to pick another' : ''}
</Text>
<ul style={{ margin: 0, paddingInlineStart: 'var(--spacing-12)' }}>
{sorted.map((day) => (
<li key={day}><Text style={{ color: 'var(--label-secondary)' }}>{dateFormat.format(new Date(day))}</Text></li>
))}
</ul>
</div>
</div>
);
}Форма/интеграция
Форма/интеграция: отправка даты с формой
У Calendar нет собственного значения формы и required: храните ISO-день в состоянии, отправляйте его через скрытый input и читайте обратно через FormData. Проверка на пустое значение выполняется при отправке, а её сообщение связано с сеткой дней через aria-describedby; кнопка сброса очищает состояние через событие reset формы.
import {
Button,
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
Fieldset,
FieldsetLegend,
HelperText,
Text,
} from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';
export default function SubmitTheDateWithAForm() {
const [date, setDate] = useState('');
const [missing, setMissing] = useState(false);
const [submitted, setSubmitted] = useState('');
const errorId = useId();
const pick = (next: string) => {
setDate(next);
setMissing(false);
};
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const appointment = String(data.get('appointment'));
if (!appointment) {
setMissing(true);
return;
}
setSubmitted(appointment);
};
const reset = () => {
setDate('');
setMissing(false);
setSubmitted('');
};
return (
<form onSubmit={submit} onReset={reset} style={{ display: 'grid', gap: 'var(--spacing-12)', justifyItems: 'start', width: '100%' }}>
<Fieldset>
<FieldsetLegend>Appointment date</FieldsetLegend>
<Calendar locale='en-US' value={date} onChangeValue={pick}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Appointment date' aria-describedby={missing ? errorId : undefined}>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
{missing && <HelperText id={errorId} error>Pick a date for the appointment.</HelperText>}
</Fieldset>
<input type='hidden' name='appointment' value={date} />
<div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
<Button type='submit'>Book</Button>
<Button type='reset' variant='secondary' appearance='ghost'>Reset</Button>
</div>
<Text aria-live='polite'>{submitted ? `Sent appointment=${submitted}` : ''}</Text>
</form>
);
}Раскладка/оверлей
Раскладка/оверлей: календарь в поповере
Выбор даты из триггера и компактного календаря в Popover. Поповер контролируемый, поэтому закрывается сразу после выбора дня; доступное имя триггера сохраняет название поля рядом с выбранной датой.
import {
Button,
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
Icon,
Popover,
PopoverContent,
PopoverTrigger,
} from '@vega-ui/react';
import { CalendarIcon } from '@vega-ui/icons';
import { useState } from 'react';
const dateFormat = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeZone: 'UTC' });
export default function CalendarInAPopover() {
const [open, setOpen] = useState(false);
const [date, setDate] = useState('');
const pick = (next: string) => {
setDate(next);
setOpen(false);
};
return (
<Popover open={open} onChangeOpen={setOpen} placement='bottom-start' role='dialog'>
<PopoverTrigger asChild>
<Button variant='secondary' appearance='ghost' aria-label={date ? `Due date, ${dateFormat.format(new Date(date))}` : 'Due date'}>
<Icon size='sm'><CalendarIcon /></Icon>
{date ? dateFormat.format(new Date(date)) : 'Pick a due date'}
</Button>
</PopoverTrigger>
<PopoverContent aria-label='Choose a due date'>
<Calendar compact locale='en-US' value={date} onChangeValue={pick}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Due date'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
</PopoverContent>
</Popover>
);
}Ошибка
Ошибка: диапазон через заблокированный день
disabledDates не даёт выбрать заблокированный день, но диапазон всё равно может его охватывать. Проверяйте диапазон в приложении, показывайте ошибку, только пока значение неверно, и связывайте её с сеткой дней через aria-describedby.
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
HelperText,
} from '@vega-ui/react';
import { useId, useState } from 'react';
const soldOut = ['2026-11-14', '2026-11-15'];
const dateFormat = new Intl.DateTimeFormat('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' });
function findSoldOut(start: string, end: string) {
if (!start || !end) return [];
return soldOut.filter((day) => day >= start && day <= end);
}
export default function RangeAcrossABlockedDay() {
const [[start, end], setRange] = useState<[string, string]>(['2026-11-12', '2026-11-17']);
const errorId = useId();
const blocked = findSoldOut(start, end);
const invalid = blocked.length > 0;
const blockedLabel = blocked.map((day) => dateFormat.format(new Date(day))).join(' and ');
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
<Calendar locale='en-US' selection='range' disabledDates={soldOut} value={[start, end]} onChangeValue={setRange}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Room dates' aria-describedby={invalid ? errorId : undefined}>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
{invalid && (
<HelperText id={errorId} error>
The room is sold out on {blockedLabel}. Pick dates before or after them.
</HelperText>
)}
</div>
);
}Отключённый
Отключённый: весь календарь во время сохранения
disabled выключает выбор и навигацию во всех видах, здесь — пока сохраняется новая дата встречи. Статус рядом с календарём объясняет, что происходит; один отключённый календарь этого не говорит.
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
Spinner,
Text,
} from '@vega-ui/react';
import { useState } from 'react';
const dateFormat = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeZone: 'UTC' });
function saveMeeting(date: string) {
return new Promise<string>((resolve) => setTimeout(() => resolve(date), 1500));
}
export default function WholeCalendarWhileSaving() {
const [date, setDate] = useState('2026-11-02');
const [saving, setSaving] = useState(false);
const reschedule = async (next: string) => {
setDate(next);
setSaving(true);
await saveMeeting(next);
setSaving(false);
};
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
<Calendar locale='en-US' disabled={saving} value={date} onChangeValue={reschedule}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Meeting date'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
<div aria-live='polite' style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
<Text>
{saving ? `Moving the meeting to ${dateFormat.format(new Date(date))}…` : `Meeting on ${dateFormat.format(new Date(date))}`}
</Text>
{saving && <Spinner size={2} aria-label='Saving' />}
</div>
</div>
);
}Отключённый: окно бронирования, выходные и праздники
Дни до min и после max отключены, а кнопки «назад» / «вперёд» скрываются на этих месяцах. Предикат disabledDates закрывает выходные и список праздников; если нужен только список, подойдёт и массив ISO-дней. Правило написано рядом с календарём и связано через aria-describedby, потому что отключённый день не объясняет причину.
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
HelperText,
} from '@vega-ui/react';
import { useId } from 'react';
const holidays = ['2026-12-24', '2026-12-25', '2026-12-31'];
function isClosed(date: string) {
const weekday = new Date(date).getUTCDay();
if (weekday === 0 || weekday === 6) return true;
return holidays.includes(date);
}
export default function BookingWindowAndBlockedDays() {
const hintId = useId();
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
<Calendar locale='en-US' min='2026-12-01' max='2027-01-31' defaultMonth='2026-12-01' disabledDates={isClosed}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Pickup date' aria-describedby={hintId}>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
<HelperText id={hintId}>
Pickup runs on weekdays from December 1 to January 31, except December 24, 25 and 31.
</HelperText>
</div>
);
}Пограничный случай
Пограничный случай: дни соседних месяцев
includeOverflowDays заполняет пустые ячейки днями предыдущего и следующего месяцев. Они видны, но их нельзя выбрать; шаблон дня стилизует их через inMonth.
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
useCalendarDayContext,
} from '@vega-ui/react';
function OverflowDay() {
const { inMonth } = useCalendarDayContext();
if (inMonth) return <CalendarDay />;
return <CalendarDay style={{ color: 'var(--label-quaternary)' }} />;
}
export default function OverflowDays() {
return (
<Calendar locale='en-US' defaultMonth='2026-02-01'>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Days'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks includeOverflowDays>
<OverflowDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
);
}Пограничный случай: min и max в одном месяце
Когда min и max попадают в один месяц, обе кнопки «назад» и «вперёд» скрываются и выбрать можно только дни между ними; фокус клавиатуры начинается с первого доступного дня. Календарь сам не переходит к min: без defaultMonth он открывается на текущем месяце, даже если тот вне диапазона.
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
HelperText,
} from '@vega-ui/react';
import { useId } from 'react';
const conference = { min: '2026-11-10', max: '2026-11-20' };
export default function MinAndMaxInsideOneMonth() {
const hintId = useId();
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
<Calendar locale='en-US' min={conference.min} max={conference.max} defaultMonth={conference.min}>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Session day' aria-describedby={hintId}>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
<HelperText id={hintId}>Sessions run from November 10 to 20.</HelperText>
</div>
);
}Пограничный случай: год из 13 месяцев
С календарной системой из createCalendarSystem (см. Адаптер календарной системы) вид месяцев следует этому календарю. Еврейский год 5784 — високосный, поэтому CalendarMonths показывает 13 месяцев, включая Адар I и Адар II. Значение по-прежнему ISO-день, как видно из подписи.
import { createCalendarSystem } from '@vega-ui/i18n';
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonth,
CalendarMonthLabel,
CalendarMonths,
CalendarNextButton,
CalendarPrevButton,
CalendarViewButton,
CalendarViewButtonGroup,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYear,
CalendarYearLabel,
CalendarYears,
Text,
} from '@vega-ui/react';
import { useState } from 'react';
const hebrewCalendar = createCalendarSystem('hebrew');
const hebrewDate = new Intl.DateTimeFormat('en-US-u-ca-hebrew', { dateStyle: 'long', timeZone: 'UTC' });
export default function ThirteenMonthYear() {
const [date, setDate] = useState('2024-03-11');
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
<Calendar calendar={hebrewCalendar} locale='en-US' value={date} onChangeValue={setDate} defaultView='month'>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous' />
<CalendarViewButtonGroup>
<CalendarViewButton view='month'><CalendarMonthLabel /></CalendarViewButton>
<CalendarViewButton view='year'><CalendarYearLabel /></CalendarViewButton>
</CalendarViewButtonGroup>
<CalendarNextButton aria-label='Next' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Days'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
<CalendarMonths aria-label='Months' cols={4}>
<CalendarMonth />
</CalendarMonths>
<CalendarYears>
<CalendarYear />
</CalendarYears>
</CalendarContent>
</Calendar>
<Text aria-live='polite' style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>
{date ? `${date} is ${hebrewDate.format(new Date(date))}` : 'Pick a day'}
</Text>
</div>
);
}Анатомия
Обзор
Calendar владеет видимым месяцем, текущим видом, состоянием выбора, правилами отключённых дат, локалью и календарной системой, а также координирует клавиатуру между видами дней, месяцев и лет.
Обязательные части
Calendar
Корневой провайдер модели выбора даты.
- Обязательна: да
- Владеет: состоянием вида, видимым месяцем, состоянием выбора, логикой отключённых дат,
localeиcalendar
Необязательные части
CalendarHeader
Обёртка заголовка для навигации и переключения видов.
CalendarPrevButton / CalendarNextButton
Показывают предыдущую или следующую страницу текущего вида: предыдущий или следующий месяц в днях, предыдущую или следующую страницу лет в годах. Скрыты в виде месяцев, а в днях — когда предыдущий или следующий месяц выходит за min / max.
CalendarViewButtonGroup
Держит кнопки видов вместе между кнопками «назад» и «вперёд».
CalendarViewButton
view='month' или view='year' переключает этот вид; повторное нажатие возвращает к дням. Кнопка нажата (aria-pressed), пока показан её вид.
CalendarMonthLabel / CalendarYearLabel
Видимые месяц и год в локали и календарной системе корня.
CalendarContent
Основная обёртка содержимого для видов.
CalendarDays
Вид дней. Его дочерние элементы — CalendarWeekdays и CalendarWeeks.
CalendarWeekdays
Строка названий дней недели в локали корня, начиная с первого дня её недели. Проп: format ('long' | 'short' | 'narrow', по умолчанию 'short'). Обязательные дочерние элементы — шаблон дня недели, обычно <CalendarWeekday />; день недели читается через useCalendarWeekdayContext().
CalendarWeeks
Сетка дней по неделям с переключением месяцев свайпом. Пропсы: format, includeOverflowDays.
CalendarMonths
Вид месяцев: месяцы видимого года, 12 или 13 в зависимости от календарной системы. Пропсы: cols, format.
CalendarYears
Вид лет: страницы лет, переключаемые свайпом. Проп: format.
CalendarDay / CalendarMonth / CalendarYear
Шаблоны ячеек. Дочерние элементы CalendarWeeks, CalendarMonths и CalendarYears обязательны и рендерятся по одному разу на ячейку внутри контекста ячейки; обычно это <CalendarDay />, <CalendarMonth /> или <CalendarYear />, которые показывают отформатированную подпись. Ячейка читается через useCalendarDayContext(), useCalendarMonthContext() или useCalendarYearContext().
Порядок композиции
Типичная композиция:
CalendarCalendarHeaderс кнопками навигации и кнопками видовCalendarContentCalendarDays,CalendarMonths,CalendarYears; видим только текущий вид
Допустимые варианты композиции
Виды месяцев и лет необязательны. Календарь только с днями оставляет в CalendarContent один CalendarDays и показывает месяц и год простым текстом между кнопками навигации; без CalendarMonths и CalendarYears кнопке CalendarViewButton нечего открывать, поэтому её не используют. Полная композиция — в разделе Минимальная композиция.
import {
Calendar,
CalendarContent,
CalendarDay,
CalendarDays,
CalendarHeader,
CalendarMonthLabel,
CalendarNextButton,
CalendarPrevButton,
CalendarWeekday,
CalendarWeekdays,
CalendarWeeks,
CalendarYearLabel,
Text,
} from '@vega-ui/react';
export default function ValidCompositionPatterns() {
return (
<Calendar locale='en-US'>
<CalendarHeader>
<CalendarPrevButton aria-label='Previous month' />
<Text fontWeight={600}>
<CalendarMonthLabel /> <CalendarYearLabel />
</Text>
<CalendarNextButton aria-label='Next month' />
</CalendarHeader>
<CalendarContent>
<CalendarDays aria-label='Days'>
<CalendarWeekdays>
<CalendarWeekday />
</CalendarWeekdays>
<CalendarWeeks>
<CalendarDay />
</CalendarWeeks>
</CalendarDays>
</CalendarContent>
</Calendar>
);
}Недопустимые варианты композиции
Части видов вне Calendar
Виды и шаблоны ячеек зависят от контекста корня. Если вынести их за пределы Calendar, координация ломается.
Пользовательские шаблоны ячеек без CalendarDay
Шаблон должен рендерить CalendarDay (или CalendarMonth, CalendarYear) в качестве ячейки, а содержимое — внутри него. Без этого сетка теряет состояния выбора, фокуса и отключения.
Виды дней, месяцев и лет как отдельные виджеты
Это согласованные виды одной модели состояния календаря, а не несвязанные компоненты.