import {
CalendarBase,
CalendarBaseControlIconButton,
CalendarBaseHeader,
CalendarBasePickerButton,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
DayPicker,
DayPickerCell,
DayPickerLayout,
Icon,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
const WEEK = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
<CalendarBase size='sm'>
<CalendarBaseHeader>
<CalendarBaseControlIconButton aria-label='Previous month'><Icon><ChevronLeft /></Icon></CalendarBaseControlIconButton>
<CalendarBasePickerButton>November 2026</CalendarBasePickerButton>
<CalendarBaseControlIconButton aria-label='Next month'><Icon><ChevronRight /></Icon></CalendarBaseControlIconButton>
</CalendarBaseHeader>
<CalendarBaseWeekLabels>
{WEEK.map((day) => <CalendarBaseWeekLabel key={day}>{day}</CalendarBaseWeekLabel>)}
</CalendarBaseWeekLabels>
<DayPicker locale='en-GB' month='2026-11-01' aria-label='November 2026' defaultSelected='2026-11-03'>
<DayPickerLayout includeOverflowDays><DayPickerCell /></DayPickerLayout>
</DayPicker>
</CalendarBase>- Поддержка браузеров
- Недавно доступно, 2024
- Chrome 125
- Edge 125
- Firefox 128
- Safari 18
- Safari на iOS 18
- Размер в бандле
- Протестирован в
- 18 тестов · 6 нояб.
Кратко
CalendarBase — общая основа раскладки и контекста для семейства календарей VegaUI. Он передаёт размер, вариант и компактность общим частям: заголовку, кнопкам пикера, кнопкам управления и подписям дней недели, — но сам по себе не является полноценным виджетом выбора даты.
Импорт
import {
CalendarBase,
CalendarBaseControlIconButton,
CalendarBaseHeader,
CalendarBasePickerButton,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
type CalendarBaseControlIconButtonProps,
type CalendarBaseHeaderProps,
type CalendarBasePickerButtonProps,
type CalendarBaseProps,
type CalendarBaseSize,
type CalendarBaseVariant,
type CalendarBaseWeekLabelProps,
type CalendarBaseWeekLabelsProps,
} from '@vega-ui/react';Экспортируемые типы
CalendarBasePropsCalendarBaseHeaderPropsCalendarBaseWeekLabelsPropsCalendarBaseWeekLabelPropsCalendarBasePickerButtonPropsCalendarBaseControlIconButtonPropsCalendarBaseVariantCalendarBaseSize
Минимальная композиция
import {
CalendarBase,
CalendarBaseHeader,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
DayPicker,
DayPickerCell,
DayPickerLayout,
Text,
} from '@vega-ui/react';
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
export default function MinimalComposition() {
return (
<CalendarBase compact>
<CalendarBaseHeader>
<Text fontWeight={600}>November 2026</Text>
</CalendarBaseHeader>
<DayPicker aria-label='November 2026' locale='en-US' month='2026-11-01'>
<CalendarBaseWeekLabels>
{weekdays.map((weekday) => (
<CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
))}
</CalendarBaseWeekLabels>
<DayPickerLayout weekStartsOn={0}>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
</CalendarBase>
);
}Обязательные части
CalendarBase: корневая раскладка и общий контекст календаря
Необязательные части
CalendarBaseHeaderCalendarBasePickerButtonCalendarBaseControlIconButtonCalendarBaseWeekLabelsCalendarBaseWeekLabel- встроенное содержимое пикера из
DayPicker,MonthPicker,YearPickerили собственных обёрток семейства календаря
Порядок композиции
CalendarBase- необязательный
CalendarBaseHeader - необязательная строка подписей дней недели
- встроенное содержимое пикера/сетки
Варианты
- Вариант:
primaryилиsecondary - Размер:
xs,sm,md,lg,xl - Плотность: по умолчанию или
compact
Пограничные случаи
CalendarBase— прежде всего примитив подсистемы, а не основная поверхность календаря для пользователей.- Размер и вариант должны совпадать у заголовка и встроенного содержимого пикера.
compactменяет плотность раскладки, но не логику работы с датами.- Он должен визуально соответствовать слоям пикера, а не становиться отдельной темой или системой раскладки.
Частые ошибки
- Считать
CalendarBaseполноценным виджетом календаря. - Вручную пересобирать общие части заголовка календаря вместо экспортированных.
- Допускать расхождение собственных вариантов с остальным семейством календаря.
Примеры
Базовый
Базовый: календарь дней из базовых частей
CalendarBase — оболочка для календаря, который приложение собирает само: она передаёт size и variant кнопкам заголовка и подписям дней недели, а месяцем управляет приложение. Строка дней недели находится внутри DayPicker, потому что это строка сетки, и её порядок совпадает с weekStartsOn раскладки. DayPicker сохраняет активную ячейку при смене month, поэтому пример переносит active вместе с месяцем, чтобы в порядке табуляции оставалась ячейка.
import {
CalendarBase,
CalendarBaseControlIconButton,
CalendarBaseHeader,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
DayPicker,
DayPickerCell,
DayPickerLayout,
Icon,
Text,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useState } from 'react';
const LOCALE = 'en-GB';
const WEEK_START = 1;
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'short', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(Date.UTC(2026, 10, 1 + WEEK_START + index)));
function addMonths(month: string, amount: number) {
const date = new Date(month);
date.setUTCMonth(date.getUTCMonth() + amount);
return date.toISOString().slice(0, 10);
}
export default function DayCalendarFromBaseParts() {
const [month, setMonth] = useState('2026-11-01');
const [active, setActive] = useState('2026-11-01');
const [selected, setSelected] = useState<string | null>(null);
const showMonth = (amount: number) => {
const next = addMonths(month, amount);
setMonth(next);
setActive(next);
};
return (
<CalendarBase compact>
<CalendarBaseHeader>
<CalendarBaseControlIconButton aria-label='Previous month' onClick={() => showMonth(-1)}>
<Icon><ChevronLeft /></Icon>
</CalendarBaseControlIconButton>
<Text fontWeight={600} aria-live='polite' style={{ alignSelf: 'center' }}>
{monthFormat.format(new Date(month))}
</Text>
<CalendarBaseControlIconButton aria-label='Next month' onClick={() => showMonth(1)}>
<Icon><ChevronRight /></Icon>
</CalendarBaseControlIconButton>
</CalendarBaseHeader>
<DayPicker
aria-label={monthFormat.format(new Date(month))}
locale={LOCALE}
month={month}
active={active}
onChangeActive={setActive}
selected={selected}
onSelectCell={setSelected}
>
<CalendarBaseWeekLabels>
{weekdays.map((weekday) => (
<CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
))}
</CalendarBaseWeekLabels>
<DayPickerLayout weekStartsOn={WEEK_START}>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
</CalendarBase>
);
}Контролируемый/с состоянием
Контролируемый/с состоянием: заголовок с динамическими подписями
Одна CalendarBasePickerButton переключает дни и месяцы и сообщает состояние через aria-pressed; подписи кнопок «назад» / «вперёд» меняются вместе с видом, потому что те же кнопки шагают на месяц или на год.
import {
CalendarBase,
CalendarBaseControlIconButton,
CalendarBaseHeader,
CalendarBasePickerButton,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
DayPicker,
DayPickerCell,
DayPickerLayout,
Icon,
MonthPicker,
MonthPickerCell,
MonthPickerLayout,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useState } from 'react';
export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];
const WEEK_START = 0;
function addMonths(month: string, amount: number) {
const date = new Date(month);
date.setUTCMonth(date.getUTCMonth() + amount);
return date.toISOString().slice(0, 10);
}
export default function HeaderWithDynamicLabels({ locale = 'en-US' }: { locale?: string }) {
const weekdayFormat = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(locale, { month: 'long', timeZone: 'UTC' });
const yearFormat = new Intl.DateTimeFormat(locale, { year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(Date.UTC(2026, 10, 1 + WEEK_START + index)));
const [view, setView] = useState<'day' | 'month'>('day');
const [month, setMonth] = useState('2026-11-01');
const [active, setActive] = useState('2026-11-01');
const step = view === 'day' ? 1 : 12;
const showMonth = (next: string) => {
setMonth(next);
setActive(next);
};
const pickMonth = (next: string) => {
showMonth(next);
setView('day');
};
const toggleView = () => {
setActive(month);
setView(view === 'day' ? 'month' : 'day');
};
return (
<CalendarBase compact>
<CalendarBaseHeader>
<CalendarBaseControlIconButton
aria-label={view === 'day' ? 'Previous month' : 'Previous year'}
onClick={() => showMonth(addMonths(month, -step))}
>
<Icon><ChevronLeft /></Icon>
</CalendarBaseControlIconButton>
<CalendarBasePickerButton aria-pressed={view === 'month'} onClick={toggleView}>
{monthFormat.format(new Date(month))} {yearFormat.format(new Date(month))}
</CalendarBasePickerButton>
<CalendarBaseControlIconButton
aria-label={view === 'day' ? 'Next month' : 'Next year'}
onClick={() => showMonth(addMonths(month, step))}
>
<Icon><ChevronRight /></Icon>
</CalendarBaseControlIconButton>
</CalendarBaseHeader>
{view === 'day' && (
<DayPicker aria-label='Days' locale={locale} month={month} active={active} onChangeActive={setActive}>
<CalendarBaseWeekLabels>
{weekdays.map((weekday) => (
<CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
))}
</CalendarBaseWeekLabels>
<DayPickerLayout weekStartsOn={WEEK_START}>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
)}
{view === 'month' && (
<MonthPicker
aria-label='Months'
locale={locale}
year={month}
active={active}
onChangeActive={setActive}
selected={month}
onSelectCell={pickMonth}
>
<MonthPickerLayout format='short'>
<MonthPickerCell />
</MonthPickerLayout>
</MonthPicker>
)}
</CalendarBase>
);
}Раскладка/оверлей
Раскладка/оверлей: два месяца в одной оболочке
Две DayPickerLayout внутри одного DayPicker позволяют диапазону пересечь границу месяца; разные offset сохраняют индексы строк уникальными. У каждого месяца своя строка дней недели и aria-label на группе строк, поэтому видимые заголовки месяцев можно скрыть от скринридера.
import {
CalendarBase,
CalendarBaseControlIconButton,
CalendarBaseHeader,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
DayPicker,
DayPickerCell,
DayPickerLayout,
Icon,
Text,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useState } from 'react';
export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];
const WEEK_START = 0;
function addMonths(month: string, amount: number) {
const date = new Date(month);
date.setUTCMonth(date.getUTCMonth() + amount);
return date.toISOString().slice(0, 10);
}
export default function TwoMonthRange({ locale = 'en-US' }: { locale?: string }) {
const weekdayFormat = new Intl.DateTimeFormat(locale, { weekday: 'narrow', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' });
const rangeFormat = new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(Date.UTC(2026, 10, 1 + WEEK_START + index)));
const [month, setMonth] = useState('2026-11-01');
const [active, setActive] = useState('2026-11-01');
const [range, setRange] = useState<string[]>([]);
const nextMonth = addMonths(month, 1);
const [start, end] = range;
const showMonth = (amount: number) => {
const next = addMonths(month, amount);
setMonth(next);
setActive(next);
};
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
<CalendarBase>
<CalendarBaseHeader>
<CalendarBaseControlIconButton aria-label='Previous month' onClick={() => showMonth(-1)}>
<Icon><ChevronLeft /></Icon>
</CalendarBaseControlIconButton>
<CalendarBaseControlIconButton aria-label='Next month' onClick={() => showMonth(1)}>
<Icon><ChevronRight /></Icon>
</CalendarBaseControlIconButton>
</CalendarBaseHeader>
<DayPicker
aria-label='Trip dates'
locale={locale}
selection='range'
month={month}
active={active}
onChangeActive={setActive}
selected={range}
onSelectCell={setRange}
style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 'var(--spacing-16)', width: '100%' }}
>
{[month, nextMonth].map((visible, index) => (
<div key={visible} role='presentation' style={{ display: 'grid', gap: 'var(--spacing-4)' }}>
<Text aria-hidden fontWeight={600} style={{ textAlign: 'center' }}>{monthFormat.format(new Date(visible))}</Text>
<CalendarBaseWeekLabels>
{weekdays.map((weekday, col) => (
<CalendarBaseWeekLabel key={col}>{weekday}</CalendarBaseWeekLabel>
))}
</CalendarBaseWeekLabels>
<DayPickerLayout
aria-label={monthFormat.format(new Date(visible))}
month={visible}
offset={index}
weekStartsOn={WEEK_START}
>
<DayPickerCell />
</DayPickerLayout>
</div>
))}
</DayPicker>
</CalendarBase>
<Text aria-live='polite' style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>
{start && end ? rangeFormat.formatRange(new Date(start), new Date(end)) : 'Pick departure and return'}
</Text>
</div>
);
}Отключённый
Отключённый: навигация внутри окна
from и to у DayPicker отключают дни вне окна, а приложение отключает кнопки «назад» / «вперёд» на первом и последнем месяце. Правило написано рядом с сеткой и связано через aria-describedby.
import {
CalendarBase,
CalendarBaseControlIconButton,
CalendarBaseHeader,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
DayPicker,
DayPickerCell,
DayPickerLayout,
HelperText,
Icon,
Text,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useId, useState } from 'react';
const LOCALE = 'en-US';
const WEEK_START = 0;
const FIRST_DAY = '2026-11-16';
const LAST_DAY = '2027-01-15';
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'short', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(Date.UTC(2026, 10, 1 + WEEK_START + index)));
function addMonths(month: string, amount: number) {
const date = new Date(month);
date.setUTCMonth(date.getUTCMonth() + amount);
return date.toISOString().slice(0, 10);
}
function getMonthStart(day: string) {
return `${day.slice(0, 7)}-01`;
}
export default function NavigationWithinAWindow() {
const [month, setMonth] = useState(getMonthStart(FIRST_DAY));
const [active, setActive] = useState(FIRST_DAY);
const hintId = useId();
const isFirstMonth = month === getMonthStart(FIRST_DAY);
const isLastMonth = month === getMonthStart(LAST_DAY);
const showMonth = (amount: number) => {
const next = addMonths(month, amount);
setMonth(next);
setActive(next < FIRST_DAY ? FIRST_DAY : next);
};
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
<CalendarBase compact>
<CalendarBaseHeader>
<CalendarBaseControlIconButton aria-label='Previous month' disabled={isFirstMonth} onClick={() => showMonth(-1)}>
<Icon><ChevronLeft /></Icon>
</CalendarBaseControlIconButton>
<Text fontWeight={600} style={{ alignSelf: 'center' }}>{monthFormat.format(new Date(month))}</Text>
<CalendarBaseControlIconButton aria-label='Next month' disabled={isLastMonth} onClick={() => showMonth(1)}>
<Icon><ChevronRight /></Icon>
</CalendarBaseControlIconButton>
</CalendarBaseHeader>
<DayPicker
aria-label='Return date'
aria-describedby={hintId}
locale={LOCALE}
month={month}
from={FIRST_DAY}
to={LAST_DAY}
active={active}
onChangeActive={setActive}
>
<CalendarBaseWeekLabels>
{weekdays.map((weekday) => (
<CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
))}
</CalendarBaseWeekLabels>
<DayPickerLayout weekStartsOn={WEEK_START}>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
</CalendarBase>
<HelperText id={hintId}>Returns are accepted from November 16 to January 15.</HelperText>
</div>
);
}Пограничный случай
Пограничный случай: компактные подписи дней недели
Узкие названия дней недели во многих локалях повторяют буквы (S, M, T, W, T, F, S), поэтому каждая подпись получает полное название в aria-label. compact с size='xs' держит оболочку не шире сетки.
import {
CalendarBase,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
DayPicker,
DayPickerCell,
DayPickerLayout,
} from '@vega-ui/react';
const LOCALE = 'en-US';
const WEEK_START = 0;
const narrowFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'narrow', timeZone: 'UTC' });
const longFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'long', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => {
const date = Date.UTC(2026, 10, 1 + WEEK_START + index);
return { narrow: narrowFormat.format(date), long: longFormat.format(date) };
});
export default function CompactWeekdayLabels() {
return (
<CalendarBase compact size='xs'>
<DayPicker aria-label='November 2026' locale={LOCALE} month='2026-11-01'>
<CalendarBaseWeekLabels>
{weekdays.map(({ narrow, long }) => (
<CalendarBaseWeekLabel key={long} aria-label={long} title={long}>{narrow}</CalendarBaseWeekLabel>
))}
</CalendarBaseWeekLabels>
<DayPickerLayout weekStartsOn={WEEK_START}>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
</CalendarBase>
);
}Пограничный случай: оболочка справа налево
dir='rtl' на CalendarBase отражает заголовок и сетку, но базовые части не меняют иконки местами: кнопке «назад» нужен шеврон вправо, а кнопке «вперёд» — влево. Кнопки навигации Calendar разворачивают шевроны сами.
import {
CalendarBase,
CalendarBaseControlIconButton,
CalendarBaseHeader,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
DayPicker,
DayPickerCell,
DayPickerLayout,
Icon,
Text,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useState } from 'react';
const LOCALE = 'he-IL';
const WEEK_START = 0;
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'narrow', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(Date.UTC(2026, 10, 1 + WEEK_START + index)));
function addMonths(month: string, amount: number) {
const date = new Date(month);
date.setUTCMonth(date.getUTCMonth() + amount);
return date.toISOString().slice(0, 10);
}
export default function RightToLeftShell() {
const [month, setMonth] = useState('2026-11-01');
const [active, setActive] = useState('2026-11-01');
const showMonth = (amount: number) => {
const next = addMonths(month, amount);
setMonth(next);
setActive(next);
};
return (
<CalendarBase compact dir='rtl' lang={LOCALE}>
<CalendarBaseHeader>
<CalendarBaseControlIconButton aria-label='החודש הקודם' onClick={() => showMonth(-1)}>
<Icon><ChevronRight /></Icon>
</CalendarBaseControlIconButton>
<Text fontWeight={600} style={{ alignSelf: 'center' }}>{monthFormat.format(new Date(month))}</Text>
<CalendarBaseControlIconButton aria-label='החודש הבא' onClick={() => showMonth(1)}>
<Icon><ChevronLeft /></Icon>
</CalendarBaseControlIconButton>
</CalendarBaseHeader>
<DayPicker aria-label='ימים' locale={LOCALE} month={month} active={active} onChangeActive={setActive}>
<CalendarBaseWeekLabels>
{weekdays.map((weekday, col) => (
<CalendarBaseWeekLabel key={col}>{weekday}</CalendarBaseWeekLabel>
))}
</CalendarBaseWeekLabels>
<DayPickerLayout weekStartsOn={WEEK_START}>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
</CalendarBase>
);
}Анатомия
Обзор
CalendarBase даёт общую визуальную оболочку для компонентов семейства календаря. Он передаёт контекст общему заголовку, кнопкам управления, кнопкам пикера и подписям дней недели.
Обязательные части
CalendarBase
Обязательна. Отвечает за общий размер, вариант и компактность.
Необязательные части
CalendarBaseHeader
Необязательная строка заголовка для навигации и триггеров пикера.
CalendarBasePickerButton
Необязательная кнопка для переключения пикера месяца/года.
CalendarBaseControlIconButton
Необязательная кнопка «назад»/«вперёд».
CalendarBaseWeekLabels
Необязательная обёртка-строка для подписей дней недели.
CalendarBaseWeekLabel
Необязательная ячейка заголовка дня недели.
Порядок композиции
CalendarBase- необязательный
CalendarBaseHeader - встроенное содержимое пикера
- необязательный
CalendarBaseWeekLabelsвнутри пикера перед его строками: у строки подписейrole='row', и она принадлежит сетке
Допустимые паттерны композиции
Рабочий календарь дней: заголовок с кнопками-иконками и подписью месяца, затем DayPicker, в котором строка дней недели и раскладка. Видимый месяц и активный день хранит приложение.
import {
CalendarBase,
CalendarBaseControlIconButton,
CalendarBaseHeader,
CalendarBaseWeekLabel,
CalendarBaseWeekLabels,
DayPicker,
DayPickerCell,
DayPickerLayout,
Icon,
Text,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useState } from 'react';
const LOCALE = 'en-GB';
const WEEK_START = 1;
const weekdayFormat = new Intl.DateTimeFormat(LOCALE, { weekday: 'short', timeZone: 'UTC' });
const monthFormat = new Intl.DateTimeFormat(LOCALE, { month: 'long', year: 'numeric', timeZone: 'UTC' });
const weekdays = Array.from({ length: 7 }, (_, index) => weekdayFormat.format(Date.UTC(2026, 10, 1 + WEEK_START + index)));
function addMonths(month: string, amount: number) {
const date = new Date(month);
date.setUTCMonth(date.getUTCMonth() + amount);
return date.toISOString().slice(0, 10);
}
export default function DayCalendarFromBaseParts() {
const [month, setMonth] = useState('2026-11-01');
const [active, setActive] = useState('2026-11-01');
const [selected, setSelected] = useState<string | null>(null);
const showMonth = (amount: number) => {
const next = addMonths(month, amount);
setMonth(next);
setActive(next);
};
return (
<CalendarBase compact>
<CalendarBaseHeader>
<CalendarBaseControlIconButton aria-label='Previous month' onClick={() => showMonth(-1)}>
<Icon><ChevronLeft /></Icon>
</CalendarBaseControlIconButton>
<Text fontWeight={600} aria-live='polite' style={{ alignSelf: 'center' }}>
{monthFormat.format(new Date(month))}
</Text>
<CalendarBaseControlIconButton aria-label='Next month' onClick={() => showMonth(1)}>
<Icon><ChevronRight /></Icon>
</CalendarBaseControlIconButton>
</CalendarBaseHeader>
<DayPicker
aria-label={monthFormat.format(new Date(month))}
locale={LOCALE}
month={month}
active={active}
onChangeActive={setActive}
selected={selected}
onSelectCell={setSelected}
>
<CalendarBaseWeekLabels>
{weekdays.map((weekday) => (
<CalendarBaseWeekLabel key={weekday}>{weekday}</CalendarBaseWeekLabel>
))}
</CalendarBaseWeekLabels>
<DayPickerLayout weekStartsOn={WEEK_START}>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
</CalendarBase>
);
}Недопустимые паттерны композиции
Общие части календаря отрендерены вне CalendarBase
Они теряют контекст размера и варианта, который сохраняет единообразие семейства.
Календарь для пользователей собран из базовых частей без повторного использования поведения семейства
Визуальная оболочка может совпадать, но взаимодействие и семантика могут разойтись с компонентами календаря более высокого уровня.