import { DayPicker, DayPickerCell, DayPickerLayout } from '@vega-ui/react';
<DayPicker aria-label='November 2026' locale='en-GB' month='2026-11-01' defaultSelected='2026-11-03'>
<DayPickerLayout><DayPickerCell /></DayPickerLayout>
</DayPicker>- Поддержка браузеров
- Широко доступно
- Chrome 111
- Edge 111
- Firefox 113
- Safari 15.4
- Safari на iOS 15.4
- Размер в бандле
- Протестирован в
- 24 теста · 6 нояб.
- Oklab and OkLCh — приходит из дизайн-токенов
- Scroll snap
- scrollend — в коде есть запасной вариант, в минимум не входит
- scrollbar-width — есть запасной вариант с префиксом -webkit-, в минимум не входит
- Intl.Locale
- Intl.Locale info — в коде есть запасной вариант, в минимум не входит
- Resize observer
- Logical properties
- :focus-visible
- :where()
- contain
- scroll-behavior
Кратко
DayPicker — низкоуровневая подсистема сетки дней, на которой строится стек календаря. Она основана на DataGridPicker, поддерживает выбор одной даты, нескольких дат и диапазона и предоставляет части раскладки, строк, элементов и скроллера.
Импорты
import {
DayPicker,
DayPickerCell,
DayPickerItem,
DayPickerLayout,
DayPickerRow,
DayPickerRowGroup,
DayPickerScroller,
DayPickerScrollerContent,
DayPickerScrollerLayout,
createDayPickerGrid,
useDayPickerCellContext,
useDayPickerContext,
useDayPickerScrollerContext,
type DayPickerGridCell,
type DayPickerGridOptions,
type DayPickerGridRow,
type DayPickerCellContextData,
type DayPickerCellProps,
type DayPickerItemProps,
type DayPickerLayoutProps,
type DayPickerProps,
type DayPickerRowGroupProps,
type DayPickerRowProps,
type DayPickerScrollerContentProps,
type DayPickerScrollerLayoutProps,
type DayPickerScrollerProps,
} from '@vega-ui/react';Экспортируемые типы
DayPickerPropsDayPickerLayoutPropsDayPickerRowGroupPropsDayPickerRowPropsDayPickerItemPropsDayPickerCellPropsDayPickerCellContextDataDayPickerScrollerPropsDayPickerScrollerContentPropsDayPickerScrollerLayoutPropsDayPickerGridCell,DayPickerGridRow,DayPickerGridOptions
Минимальная композиция
import { DayPicker, DayPickerCell, DayPickerLayout } 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 (
<DayPicker aria-label='November 2026' locale={locale} month='2026-11-01'>
<DayPickerLayout>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
);
}Обязательные части
DayPicker: корневая подсистема выбора дней
Необязательные части
DayPickerLayoutDayPickerRowGroupDayPickerRowDayPickerItemDayPickerCell: ячейка, которая ставится шаблоном раскладокDayPickerScrollerDayPickerScrollerContentDayPickerScrollerLayout
Порядок композиции
DayPicker- либо
DayPickerLayoutс шаблоном ячейки в дочерних элементах (<DayPickerCell />или собственным), либо собственная композиция строк и элементов - необязательная обёртка-скроллер для постраничной навигации по месяцам
Варианты
- Выбор:
single,multiple,range - Раскладка: статичная сетка или перелистывание месяцев на основе скроллера
- Период: явный
month(строкаYYYY-MM-DDлюбого дня в нём) или текущий месяц - Локаль и календарная система: пропсы
localeиcalendar, по умолчанию — локаль окружения и григорианский календарь
Пограничные случаи
- Включение дней соседних месяцев меняет и визуальную плотность, и поведение выбора.
- Отключённые, выбранные, исключённые и сфокусированные даты должны оставаться различимыми.
- Прокручиваемое перелистывание месяцев добавляет ещё один слой синхронизации состояния видимого периода.
- Ключи ячеек (
selected,from,to,disabled,active) — дниYYYY-MM-DDв любой календарной системе; отcalendarзависят только форма сетки и подписи.
Частые ошибки
- Математика сетки дат пишется заново вместо использования
createDayPickerGrid. - Передаются числовые годы, месяцы с отсчётом от нуля или временные метки вместо строк
YYYY-MM-DD. - Видимость выбора теряется в собственном рендере
DayPickerItem. - Компонент используется как готовый календарь, а не как низкоуровневая подсистема выбора.
Примеры
Базовый
Базовый: дни одного месяца
DayPicker — низкоуровневая сетка дней без заголовка; DayPickerLayout рендерит месяц, заданный через month. Ключи ячеек — ISO-дни, поэтому defaultSelected — тоже ISO-день. locale задаёт первый день недели, а сетке нужен aria-label, потому что у неё нет видимого заголовка.
import { DayPicker, DayPickerCell, DayPickerLayout } from '@vega-ui/react';
export default function OneMonthOfDays() {
return (
<DayPicker aria-label='March 2026' locale='en-GB' month='2026-03-01' defaultSelected='2026-03-15'>
<DayPickerLayout>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
);
}Контролируемый/с состоянием
Контролируемый/с состоянием: выбранный день
selected и onSelectCell делают выбор контролируемым; в одиночном режиме null означает «контролируемый, ничего не выбрано» — на это опирается кнопка очистки.
import {
Button,
DayPicker,
DayPickerCell,
DayPickerLayout,
Text,
} from '@vega-ui/react';
import { useState } from 'react';
export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];
export default function SelectedDay({ locale = 'en-US' }: { locale?: string }) {
const dateFormat = new Intl.DateTimeFormat(locale, { dateStyle: 'full', timeZone: 'UTC' });
const [due, setDue] = useState<string | null>('2026-11-20');
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
<DayPicker aria-label='Invoice due date, November 2026' locale={locale} month='2026-11-01' selected={due} onSelectCell={setDue}>
<DayPickerLayout>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
<Text aria-live='polite' style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>Due: {due ? dateFormat.format(new Date(due)) : 'not set'}</Text>
<Button size='sm' variant='secondary' appearance='ghost' disabled={due === null} onClick={() => setDue(null)}>Clear due date</Button>
</div>
);
}Контролируемый/с состоянием: видимый месяц с собственной навигацией
Со скроллером месяц можно листать свайпом. Храните month в состоянии и обновляйте его из onChangePeriod, чтобы собственный заголовок оставался синхронизирован со свайпами, а его кнопки могли сдвигать сетку. DayPicker не переносит активную ячейку при смене month, поэтому пример хранит active в состоянии и переводит его в новый месяц, когда прежний активный день ушёл из вида.
import {
DayPicker,
DayPickerCell,
DayPickerScroller,
DayPickerScrollerContent,
DayPickerScrollerLayout,
Icon,
IconButton,
Text,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useState } from 'react';
const monthFormat = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' });
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 VisibleMonthWithOwnNavigation() {
const [month, setMonth] = useState('2026-11-01');
const [active, setActive] = useState('2026-11-01');
const [selected, setSelected] = useState<string | null>(null);
const showMonth = (next: string) => {
setMonth(next);
setActive((current) => {
if (current.slice(0, 7) === next.slice(0, 7)) return current;
return next;
});
};
return (
<div style={{ display: 'grid', gap: 'var(--spacing-4)', inlineSize: 'fit-content' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<IconButton aria-label='Previous month' size='sm' variant='secondary' appearance='ghost' onClick={() => showMonth(addMonths(month, -1))}>
<Icon><ChevronLeft /></Icon>
</IconButton>
<Text fontWeight={600} aria-live='polite'>{monthFormat.format(new Date(month))}</Text>
<IconButton aria-label='Next month' size='sm' variant='secondary' appearance='ghost' onClick={() => showMonth(addMonths(month, 1))}>
<Icon><ChevronRight /></Icon>
</IconButton>
</div>
<DayPicker
aria-label='Pickup day'
locale='en-US'
month={month}
active={active}
onChangeActive={setActive}
selected={selected}
onSelectCell={setSelected}
>
<DayPickerScroller onChangePeriod={showMonth}>
<DayPickerScrollerContent>
<DayPickerScrollerLayout>
<DayPickerCell />
</DayPickerScrollerLayout>
</DayPickerScrollerContent>
</DayPickerScroller>
</DayPicker>
</div>
);
}Форма/интеграция
Форма/интеграция: несколько дней в форме
selection='multiple' выбирает независимые дни. У компонента нет значения формы, поэтому дни уходят скрытыми input с одним именем и читаются обратно через FormData.getAll. Проверка «хотя бы один день» выполняется при отправке и использует подсказку, связанную через aria-describedby; сброс очищает состояние через событие reset формы.
import {
Button,
DayPicker,
DayPickerCell,
DayPickerLayout,
Fieldset,
FieldsetLegend,
HelperText,
Text,
} from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';
export default function SeveralDaysInAForm() {
const [days, setDays] = useState<string[]>([]);
const [missing, setMissing] = useState(false);
const [submitted, setSubmitted] = useState<string[]>([]);
const hintId = useId();
const change = (next: string[]) => {
setDays(next);
setMissing(false);
};
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const shifts = new FormData(event.currentTarget).getAll('shift').map(String);
if (shifts.length === 0) {
setMissing(true);
return;
}
setSubmitted(shifts.sort());
};
const reset = () => {
setDays([]);
setMissing(false);
setSubmitted([]);
};
return (
<form onSubmit={submit} onReset={reset} style={{ display: 'grid', gap: 'var(--spacing-12)', justifyItems: 'start', width: '100%' }}>
<Fieldset>
<FieldsetLegend>Extra shifts in November</FieldsetLegend>
<DayPicker
aria-label='Shift days, November 2026'
aria-describedby={hintId}
locale='en-US'
selection='multiple'
month='2026-11-01'
selected={days}
onSelectCell={change}
>
<DayPickerLayout>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
<HelperText id={hintId} error={missing}>
{missing ? 'Pick at least one day.' : 'Pick every day you can work.'}
</HelperText>
</Fieldset>
{days.map((day) => (
<input key={day} type='hidden' name='shift' value={day} />
))}
<div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
<Button type='submit'>Save {days.length} {days.length === 1 ? 'shift' : 'shifts'}</Button>
<Button type='reset' variant='secondary' appearance='ghost'>Reset</Button>
</div>
<Text aria-live='polite'>{submitted.length > 0 ? `Sent shift=${submitted.join(', ')}` : ''}</Text>
</form>
);
}Раскладка/оверлей
Раскладка/оверлей: два месяца рядом
Один DayPicker может содержать несколько раскладок, поэтому диапазон можно выбрать на двух месяцах. Задайте каждой раскладке свой month и отдельный offset, чтобы индексы строк оставались уникальными во всей сетке; aria-label на каждой раскладке называет её месяц для скринридера.
import {
DayPicker,
DayPickerCell,
DayPickerLayout,
Text,
} from '@vega-ui/react';
import { useState } from 'react';
const months = ['2026-11-01', '2026-12-01'];
const monthFormat = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' });
const rangeFormat = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
export default function TwoMonthsSideBySide() {
const [range, setRange] = useState<string[]>([]);
const [start, end] = range;
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
<DayPicker
aria-label='Travel dates'
locale='en-US'
selection='range'
month={months[0]}
selected={range}
onSelectCell={setRange}
style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 'var(--spacing-16)', width: '100%' }}
>
{months.map((month, index) => (
<div key={month} role='presentation' style={{ display: 'grid', gap: 'var(--spacing-4)' }}>
<Text aria-hidden fontWeight={600} style={{ textAlign: 'center' }}>{monthFormat.format(new Date(month))}</Text>
<DayPickerLayout aria-label={monthFormat.format(new Date(month))} month={month} offset={index}>
<DayPickerCell />
</DayPickerLayout>
</div>
))}
</DayPicker>
<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>
);
}Ошибка
Ошибка: слишком длинный диапазон
Пикер диапазона сам не ограничивает его длину. Считайте ночи в приложении и превращайте подсказку, связанную с сеткой, в ошибку, пока диапазон слишком длинный.
import {
DayPicker,
DayPickerCell,
DayPickerLayout,
HelperText,
} from '@vega-ui/react';
import { useId, useState } from 'react';
const DAY = 86_400_000;
const MAX_NIGHTS = 14;
function countNights(start?: string, end?: string) {
if (!start || !end) return 0;
return (Date.parse(end) - Date.parse(start)) / DAY;
}
export default function RangeTooLong() {
const [range, setRange] = useState<string[]>(['2026-11-03', '2026-11-24']);
const messageId = useId();
const [start, end] = range;
const nights = countNights(start, end);
const invalid = nights > MAX_NIGHTS;
let message = `Up to ${MAX_NIGHTS} nights.`;
if (nights > 0) message = `${nights} nights selected. Up to ${MAX_NIGHTS} nights.`;
if (invalid) message = `${nights} nights is too long. Shorten the rental to ${MAX_NIGHTS} nights or less.`;
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
<DayPicker
aria-label='Rental period, November 2026'
aria-describedby={messageId}
locale='en-US'
selection='range'
month='2026-11-01'
selected={range}
onSelectCell={setRange}
>
<DayPickerLayout>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
<HelperText id={messageId} error={invalid} aria-live='polite'>{message}</HelperText>
</div>
);
}Отключённый
Отключённый: окно бронирования и закрытые дни
from и to отключают все дни вне окна, а предикат disabled закрывает воскресенья и праздник; если закрыты только отдельные дни, подойдёт и массив ISO-дней. Отключённые дни нельзя выбрать или сфокусировать, поэтому правило написано рядом с сеткой и связано через aria-describedby.
import {
DayPicker,
DayPickerCell,
DayPickerLayout,
HelperText,
} from '@vega-ui/react';
import { useId } from 'react';
const holidays = ['2026-11-26'];
function isClosed(day: string) {
if (new Date(day).getUTCDay() === 0) return true;
return holidays.includes(day);
}
export default function BookingWindowAndClosedWeekdays() {
const hintId = useId();
return (
<div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
<DayPicker
aria-label='Pickup day, November 2026'
aria-describedby={hintId}
locale='en-US'
month='2026-11-01'
from='2026-11-03'
to='2026-11-28'
disabled={isClosed}
>
<DayPickerLayout>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
<HelperText id={hintId}>
Pickup from November 3 to 28, Monday to Saturday. Closed on November 26 for Thanksgiving.
</HelperText>
</div>
);
}Пограничный случай
Пограничный случай: месяц другой календарной системы
С календарной системой из createCalendarSystem (см. Адаптер календарной системы) сетка показывает месяц этого календаря: в фарвардине 1405 года 31 день, он начинается с ISO-дня 2026-03-21, а неделя в fa-IR начинается с субботы. month — любой ISO-день внутри месяца; dir='rtl' на предке отражает сетку и стрелки.
import { createCalendarSystem } from '@vega-ui/i18n';
import { DayPicker, DayPickerCell, DayPickerLayout, Text } from '@vega-ui/react';
import { useState } from 'react';
const persianCalendar = createCalendarSystem('persian');
const persianMonth = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { month: 'long', year: 'numeric', timeZone: 'UTC' });
export default function MonthOfAnotherCalendarSystem() {
const [day, setDay] = useState<string | null>('2026-03-21');
return (
<div dir='rtl' lang='fa-IR' style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
<Text fontWeight={600}>{persianMonth.format(new Date('2026-04-01'))}</Text>
<DayPicker
aria-label={persianMonth.format(new Date('2026-04-01'))}
calendar={persianCalendar}
locale='fa-IR'
month='2026-04-01'
selected={day}
onSelectCell={setDay}
>
<DayPickerLayout>
<DayPickerCell />
</DayPickerLayout>
</DayPicker>
<Text dir='ltr' lang='en' style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>Stored value: {day ?? 'none'}</Text>
</div>
);
}Пограничный случай: дни соседних месяцев
includeOverflowDays заполняет пустые ячейки днями соседних месяцев. DayPickerCell рендерит их отключёнными и исключёнными из выбора; шаблон приглушает их через inMonth.
import {
DayPicker,
DayPickerCell,
DayPickerLayout,
useDayPickerCellContext,
} from '@vega-ui/react';
function OverflowDay() {
const { inMonth } = useDayPickerCellContext();
if (inMonth) return <DayPickerCell />;
return <DayPickerCell style={{ color: 'var(--label-quaternary)' }} />;
}
export default function OverflowDays() {
return (
<DayPicker aria-label='February 2026' locale='en-US' month='2026-02-01'>
<DayPickerLayout includeOverflowDays>
<OverflowDay />
</DayPickerLayout>
</DayPicker>
);
}Анатомия
Обзор
DayPicker оборачивает DataGridPicker и добавляет контекст, специфичный для сетки дней: видимый month (первый день в виде строки YYYY-MM-DD) и календарную систему calendar. Раскладка может быть статичной или на основе скроллера.
Обязательные части
DayPicker
Обязательная часть. Отвечает за режим выбора в сетке дней и контекст видимого периода.
Необязательные части
DayPickerLayout
Необязательный стандартный рендерер сетки дней.
DayPickerCell
Шаблон ячейки. Дочерние элементы DayPickerLayout и DayPickerScrollerLayout обязательны и рендерятся по разу на каждый день внутри контекста ячейки, который читается через useDayPickerCellContext(); обычно это <DayPickerCell />. Помещайте собственное содержимое внутрь DayPickerCell, чтобы сохранить состояния выбора, фокуса и отключения.
DayPickerRowGroup, DayPickerRow, DayPickerItem
Необязательные низкоуровневые части для композиции сетки.
DayPickerScroller, DayPickerScrollerContent, DayPickerScrollerLayout
Необязательная инфраструктура перелистывания на основе скроллера для навигации по месяцам.
Порядок композиции
DayPicker- необязательный слой скроллера
- либо
DayPickerLayout, либо собственная композиция строк и элементов
Допустимые варианты композиции
Композиция со скроллером: DayPickerScroller › DayPickerScrollerContent › DayPickerScrollerLayout › шаблон ячейки внутри DayPicker. Скроллер начинается с month корня и прокручивается к нему при изменении, поэтому храните month в состоянии и обновляйте его из onChangePeriod — тогда корень всегда знает видимый месяц.
import {
DayPicker,
DayPickerCell,
DayPickerScroller,
DayPickerScrollerContent,
DayPickerScrollerLayout,
} from '@vega-ui/react';
import { useState } from 'react';
export default function ValidCompositionPatterns() {
const [month, setMonth] = useState('2026-11-01');
return (
<DayPicker aria-label='Days' locale='en-US' month={month}>
<DayPickerScroller onChangePeriod={setMonth}>
<DayPickerScrollerContent>
<DayPickerScrollerLayout includeOverflowDays>
<DayPickerCell />
</DayPickerScrollerLayout>
</DayPickerScrollerContent>
</DayPickerScroller>
</DayPicker>
);
}Недопустимые варианты композиции
Собственная математика сетки вместо вспомогательных функций
Логика выбора и исключённых дней может разойтись с задуманным поведением календаря.
Композиция скроллера без согласованного контракта сопоставления периода
Видимый и отрендеренный месяцы становится трудно держать согласованными.