Перейти к содержимому
v3.0.0
English
Русский
Документация

MonthPicker

MonthPicker — подсистема выбора месяца в виде сетки, которую используют напрямую или как часть семейства календарей.
January
February
March
April
May
June
July
August
September
October
November
December
month-picker.tsx · 5 строк
import { MonthPicker, MonthPickerCell, MonthPickerLayout } from '@vega-ui/react';

<MonthPicker aria-label='Month, 2026' locale='en-US' year='2026-01-01' defaultSelected='2026-11-01' style={{ width: '100%' }}>
  <MonthPickerLayout><MonthPickerCell /></MonthPickerLayout>
</MonthPicker>
Поддержка браузеров
Широко доступно
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari на iOS 15.4
Размер в бандле
История версий
Добавлен в v1.12.0, последнее изменение в v3.0.0

Кратко

MonthPicker — подсистема выбора месяца в виде сетки, которую используют напрямую или как часть семейства календарей. Он построен на DataGridPicker и предоставляет части раскладки месяцев и кастомный рендер на основе хелперов.

Импорт

import {
  MonthPicker,
  MonthPickerCell,
  MonthPickerItem,
  MonthPickerLayout,
  MonthPickerRow,
  MonthPickerRowGroup,
  createMonthPickerGrid,
  useMonthPickerCellContext,
  useMonthPickerContext,
  type MonthPickerCellContextData,
  type MonthPickerCellProps,
  type MonthPickerItemProps,
  type MonthPickerLayoutProps,
  type MonthPickerProps,
  type MonthPickerRowGroupProps,
  type MonthPickerRowProps,
} from '@vega-ui/react';

Экспортируемые типы

  • MonthPickerProps
  • MonthPickerLayoutProps
  • MonthPickerRowGroupProps
  • MonthPickerRowProps
  • MonthPickerItemProps
  • MonthPickerCellProps
  • MonthPickerCellContextData

Минимальная композиция

en-US
ru-RU
ar-EG
he-IL
minimal-composition.tsx · 13 строк
import { MonthPicker, MonthPickerCell, MonthPickerLayout } 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 (
    <MonthPicker aria-label='Month, 2026' locale={locale} year='2026-01-01' style={{ width: '100%' }}>
      <MonthPickerLayout>
        <MonthPickerCell />
      </MonthPickerLayout>
    </MonthPicker>
  );
}

Обязательные части

  • MonthPicker: корневая подсистема выбора месяца

Необязательные части

  • MonthPickerLayout
  • MonthPickerRowGroup
  • MonthPickerRow
  • MonthPickerItem
  • MonthPickerCell: ячейка, которую помещают в раскладки как шаблон

Порядок композиции

  1. MonthPicker
  2. либо MonthPickerLayout с шаблоном ячейки в children (<MonthPickerCell /> или собственным), либо собственная композиция из строк и элементов

Варианты

  • Выбор: single, multiple, range
  • Плотность раскладки: cols; число строк зависит от количества месяцев в году
  • Период: явный year (строка YYYY-MM-DD любого дня этого года) или текущий год
  • Локаль и календарная система: пропсы locale и calendar, по умолчанию — локаль среды выполнения и григорианский календарь
  • Формат меток: названия месяцев по локали

Пограничные случаи

  • Локаль и формат меток месяцев должны совпадать с остальной системой дат.
  • Собственные раскладки должны сохранять подсказки выбора и отключённого состояния из примитивов пикера.
  • Это низкоуровневый выбор месяца, а не обязательно готовый календарь для конечного пользователя.

Частые ошибки

  • Жёстко заданные названия месяцев, когда остальное приложение локализовано.
  • Ручная сборка сетки месяцев вместо createMonthPickerGrid.
  • Предположение, что в каждом году 12 месяцев или что месяцы — числа, начинающиеся с нуля.
  • Использование компонента как обычной сетки контента, а не системы выбора месяца.

Примеры

Базовый

Базовый: месяцы одного года

MonthPickerLayout рендерит все месяцы года, заданного через year — любой ISO-день внутри него. Ключ каждой ячейки — первый день её месяца, поэтому defaultSelected для марта — '2026-03-01'. У сетки нет видимого заголовка, поэтому ей передан aria-label.

months-of-one-year.tsx · 11 строк
import { MonthPicker, MonthPickerCell, MonthPickerLayout } from '@vega-ui/react';

export default function MonthsOfOneYear() {
  return (
    <MonthPicker aria-label='Start month, 2026' locale='en-US' year='2026-01-01' defaultSelected='2026-03-01' style={{ width: '100%' }}>
      <MonthPickerLayout>
        <MonthPickerCell />
      </MonthPickerLayout>
    </MonthPicker>
  );
}

Контролируемый/с состоянием

Контролируемый/с состоянием: месяц оплаты с переключением года

Выбранный месяц и видимый год — отдельные части состояния: кнопки меняют год, клик меняет месяц, а строка ниже показывает результат. MonthPicker сохраняет активную ячейку при смене year, поэтому кнопки переносят в новый год и active.

billing-month-with-year-navigation.tsx · 52 строки
import {
  Icon,
  IconButton,
  MonthPicker,
  MonthPickerCell,
  MonthPickerLayout,
  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' });

export default function BillingMonthWithYearNavigation() {
  const [year, setYear] = useState(2026);
  const [active, setActive] = useState('2026-10-01');
  const [month, setMonth] = useState<string | null>('2026-10-01');

  const showYear = (next: number) => {
    setYear(next);
    setActive(`${next}-01-01`);
  };

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <IconButton aria-label='Previous year' size='sm' variant='secondary' appearance='ghost' onClick={() => showYear(year - 1)}>
          <Icon><ChevronLeft /></Icon>
        </IconButton>
        <Text fontWeight={600} aria-live='polite'>{year}</Text>
        <IconButton aria-label='Next year' size='sm' variant='secondary' appearance='ghost' onClick={() => showYear(year + 1)}>
          <Icon><ChevronRight /></Icon>
        </IconButton>
      </div>
      <MonthPicker
        aria-label={`Billing month, ${year}`}
        locale='en-US'
        year={`${year}-01-01`}
        active={active}
        onChangeActive={setActive}
        selected={month}
        onSelectCell={setMonth}
        style={{ width: '100%' }}
      >
        <MonthPickerLayout cols={4} format='short'>
          <MonthPickerCell />
        </MonthPickerLayout>
      </MonthPicker>
      <Text>Invoice for {month ? monthFormat.format(new Date(month)) : '—'}</Text>
    </div>
  );
}

Форма/интеграция

Форма/интеграция: период отчёта

selection='range' выбирает период из месяцев. Ячейки — начала месяцев, поэтому форма отправляет начало как есть, а конец превращает в последний день его месяца; FormData читает оба значения обратно, проверка на пустоту использует связанную подсказку, а сброс очищает состояние.

report-period.tsx · 81 строка
import {
  Button,
  Fieldset,
  FieldsetLegend,
  HelperText,
  MonthPicker,
  MonthPickerCell,
  MonthPickerLayout,
  Text,
} from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

function getMonthEnd(monthStart: string) {
  const date = new Date(monthStart);
  date.setUTCMonth(date.getUTCMonth() + 1, 0);

  return date.toISOString().slice(0, 10);
}

export default function ReportPeriod() {
  const [period, setPeriod] = useState<string[]>([]);
  const [missing, setMissing] = useState(false);
  const [query, setQuery] = useState('');
  const hintId = useId();

  const [from, to] = period;

  const change = (next: string[]) => {
    setPeriod(next);
    setMissing(false);
  };

  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);

    if (!data.get('from') || !data.get('to')) {
      setMissing(true);
      return;
    }

    setQuery(new URLSearchParams({ from: String(data.get('from')), to: String(data.get('to')) }).toString());
  };

  const reset = () => {
    setPeriod([]);
    setMissing(false);
    setQuery('');
  };

  return (
    <form onSubmit={submit} onReset={reset} style={{ display: 'grid', gap: 'var(--spacing-12)', justifyItems: 'start', width: '100%' }}>
      <Fieldset>
        <FieldsetLegend>Report period</FieldsetLegend>
        <MonthPicker
          aria-label='Report period, 2026'
          aria-describedby={hintId}
          locale='en-US'
          selection='range'
          year='2026-01-01'
          selected={period}
          onSelectCell={change}
        >
          <MonthPickerLayout cols={4} format='short'>
            <MonthPickerCell />
          </MonthPickerLayout>
        </MonthPicker>
        <HelperText id={hintId} error={missing}>
          {missing ? 'Pick the first and the last month of the period.' : 'Pick the first and the last month.'}
        </HelperText>
      </Fieldset>
      <input type='hidden' name='from' value={from ?? ''} />
      <input type='hidden' name='to' value={to ? getMonthEnd(to) : ''} />
      <div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
        <Button type='submit'>Build report</Button>
        <Button type='reset' variant='secondary' appearance='ghost'>Reset</Button>
      </div>
      <Text aria-live='polite'>{query ? `GET /reports?${query}` : ''}</Text>
    </form>
  );
}

Раскладка/оверлей

Раскладка/оверлей: фильтр по месяцу в поповере

Компактная сетка месяцев за триггером — для фильтра выписок или дашборда. Поповер контролируемый и закрывается после выбора месяца; месяцы после последней выписки отключены через to.

month-filter-in-a-popover.tsx · 49 строк
import {
  Button,
  Icon,
  MonthPicker,
  MonthPickerCell,
  MonthPickerLayout,
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@vega-ui/react';
import { ChevronDown } from '@vega-ui/icons';
import { useState } from 'react';

const triggerFormat = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' });

export default function MonthFilterInAPopover() {
  const [open, setOpen] = useState(false);
  const [month, setMonth] = useState('2026-10-01');

  const pick = (next: string) => {
    setMonth(next);
    setOpen(false);
  };

  return (
    <Popover open={open} onChangeOpen={setOpen} placement='bottom-start' role='dialog'>
      <PopoverTrigger asChild>
        <Button variant='secondary' appearance='ghost' aria-label={`Statement month, ${triggerFormat.format(new Date(month))}`}>
          {triggerFormat.format(new Date(month))}
          <Icon size='sm'><ChevronDown /></Icon>
        </Button>
      </PopoverTrigger>
      <PopoverContent aria-label='Choose a statement month'>
        <MonthPicker
          aria-label='Statement month, 2026'
          locale='en-US'
          year='2026-01-01'
          to='2026-10-01'
          selected={month}
          onSelectCell={pick}
        >
          <MonthPickerLayout cols={4} format='short'>
            <MonthPickerCell />
          </MonthPickerLayout>
        </MonthPicker>
      </PopoverContent>
    </Popover>
  );
}

Ошибка

Ошибка: слишком длинный период

Пикер не ограничивает длину диапазона. Приложение считает месяцы и превращает подсказку, связанную с сеткой, в ошибку, пока период длиннее шести месяцев.

period-too-long.tsx · 51 строка
import {
  HelperText,
  MonthPicker,
  MonthPickerCell,
  MonthPickerLayout,
} from '@vega-ui/react';
import { useId, useState } from 'react';

const MAX_MONTHS = 6;

function countMonths(from?: string, to?: string) {
  if (!from || !to) return 0;

  const years = Number(to.slice(0, 4)) - Number(from.slice(0, 4));
  const months = Number(to.slice(5, 7)) - Number(from.slice(5, 7));

  return years * 12 + months + 1;
}

export default function PeriodTooLong() {
  const [period, setPeriod] = useState<string[]>(['2026-02-01', '2026-10-01']);
  const messageId = useId();

  const [from, to] = period;
  const months = countMonths(from, to);
  const invalid = months > MAX_MONTHS;

  let message = `Compare up to ${MAX_MONTHS} months.`;
  if (months > 0) message = `${months} months selected. Compare up to ${MAX_MONTHS} months.`;
  if (invalid) message = `${months} months is too long. Pick ${MAX_MONTHS} months or fewer.`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
      <MonthPicker
        aria-label='Comparison period, 2026'
        aria-describedby={messageId}
        locale='en-US'
        selection='range'
        year='2026-01-01'
        selected={period}
        onSelectCell={setPeriod}
        style={{ width: '100%' }}
      >
        <MonthPickerLayout cols={4} format='short'>
          <MonthPickerCell />
        </MonthPickerLayout>
      </MonthPicker>
      <HelperText id={messageId} error={invalid} aria-live='polite'>{message}</HelperText>
    </div>
  );
}

Отключённый

Отключённый: месяцы без данных

from и to отключают месяцы вне периода, за который есть данные, — здесь с июня 2025 по октябрь 2026. Правило написано рядом с сеткой и связано через aria-describedby; одно отключённое состояние его не объясняет.

months-without-data.tsx · 30 строк
import {
  HelperText,
  MonthPicker,
  MonthPickerCell,
  MonthPickerLayout,
} from '@vega-ui/react';
import { useId } from 'react';

export default function MonthsWithoutData() {
  const hintId = useId();

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
      <MonthPicker
        aria-label='Statement month, 2026'
        aria-describedby={hintId}
        locale='en-US'
        year='2026-01-01'
        from='2025-06-01'
        to='2026-10-01'
        style={{ width: '100%' }}
      >
        <MonthPickerLayout cols={4} format='short'>
          <MonthPickerCell />
        </MonthPickerLayout>
      </MonthPicker>
      <HelperText id={hintId}>Statements are available from June 2025 to October 2026.</HelperText>
    </div>
  );
}

Отключённый: отдельные месяцы

Массив начал месяцев отключает ровно эти месяцы — например, те, в которые сезонная площадка закрыта.

specific-months.tsx · 31 строка
import {
  HelperText,
  MonthPicker,
  MonthPickerCell,
  MonthPickerLayout,
} from '@vega-ui/react';
import { useId } from 'react';

const closedMonths = ['2026-01-01', '2026-02-01', '2026-12-01'];

export default function SpecificMonths() {
  const hintId = useId();

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
      <MonthPicker
        aria-label='Event month, 2026'
        aria-describedby={hintId}
        locale='en-US'
        year='2026-01-01'
        disabled={closedMonths}
        style={{ width: '100%' }}
      >
        <MonthPickerLayout cols={4} format='short'>
          <MonthPickerCell />
        </MonthPickerLayout>
      </MonthPicker>
      <HelperText id={hintId}>The terrace is closed in December, January and February.</HelperText>
    </div>
  );
}

Пограничный случай

Пограничный случай: год из 13 месяцев

С календарной системой из createCalendarSystem (см. Адаптер календарной системы) сетка показывает месяцы этого календаря. Еврейский год 5784 — високосный, в нём 13 месяцев, включая Адар I и Адар II, поэтому последняя строка неполная. year='2024-03-15' — любой день этого года; его первый месяц начинается 2023-09-16, а ключ каждой ячейки — ISO-день начала её месяца, а не начало григорианского месяца: у Адара I это '2024-02-10'.

13-month-year.tsx · 28 строк
import { createCalendarSystem } from '@vega-ui/i18n';
import { MonthPicker, MonthPickerCell, MonthPickerLayout, Text } from '@vega-ui/react';
import { useState } from 'react';

const hebrewCalendar = createCalendarSystem('hebrew');

export default function ThirteenMonthYear() {
  const [month, setMonth] = useState<string | null>('2024-02-10');

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
      <MonthPicker
        aria-label='Month of 5784'
        calendar={hebrewCalendar}
        locale='en-US'
        year='2024-03-15'
        selected={month}
        onSelectCell={setMonth}
        style={{ width: '100%' }}
      >
        <MonthPickerLayout cols={4}>
          <MonthPickerCell />
        </MonthPickerLayout>
      </MonthPicker>
      <Text aria-live='polite' style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>Cell key: {month ?? 'none'}</Text>
    </div>
  );
}

Анатомия

Обзор

MonthPicker оборачивает DataGridPicker для значений-месяцев и предоставляет части строк, элементов и раскладки для статичных сеток месяцев.

Обязательные части

MonthPicker

Обязательная. Владеет моделью состояния выбора месяца через DataGridPicker.

Необязательные части

MonthPickerLayout

Необязательный рендерер сетки месяцев по умолчанию.

MonthPickerCell

Шаблон ячейки. Children у MonthPickerLayout обязательны и рендерятся по одному разу на месяц внутри контекста ячейки, который читает useMonthPickerCellContext(); обычно это <MonthPickerCell />. Помещайте собственное содержимое внутрь MonthPickerCell, чтобы сохранить состояния выбора, фокуса и отключения.

MonthPickerRowGroup, MonthPickerRow, MonthPickerItem

Необязательные низкоуровневые части для композиции сетки месяцев.

Порядок композиции

  1. MonthPicker
  2. либо MonthPickerLayout, либо собственная композиция из строк и элементов

Корректные паттерны композиции

Явные части: MonthPickerRowGroup › MonthPickerRow › MonthPickerItem, построенные через createMonthPickerGrid. MonthPickerLayout с шаблоном MonthPickerCell рендерит ту же структуру; см. Минимальная композиция.

custom-grid-rendering.tsx · 30 строк
import {
  MonthPicker,
  MonthPickerItem,
  MonthPickerRow,
  MonthPickerRowGroup,
  createMonthPickerGrid,
} from '@vega-ui/react';

const YEAR = '2026-01-01';
const narrowFormat = new Intl.DateTimeFormat('en-US', { month: 'narrow', timeZone: 'UTC' });
const longFormat = new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' });
const rows = createMonthPickerGrid({ year: YEAR, cols: 6 });

export default function CustomGridRendering() {
  return (
    <MonthPicker aria-label='Month, 2026' locale='en-US' year={YEAR} style={{ width: '100%' }}>
      <MonthPickerRowGroup>
        {rows.map(({ row, data }) => (
          <MonthPickerRow key={row} row={row}>
            {data.map(({ col, month }) => (
              <MonthPickerItem key={month} col={col} value={month} aria-label={longFormat.format(new Date(month))}>
                {narrowFormat.format(new Date(month))}
              </MonthPickerItem>
            ))}
          </MonthPickerRow>
        ))}
      </MonthPickerRowGroup>
    </MonthPicker>
  );
}

Некорректные паттерны композиции

Метки месяцев заданы вручную и расходятся с остальным приложением

Локализация и семантика дат перестают совпадать.

Раскладка пересобрана без хелперов

Порядок месяцев и семантику выбора становится проще сломать.