логотип PurpleSchool
логотип PurpleSchool

Инструкция по реализации календаря во Vue

Автор

Олег Марков

Введение

Современные веб-приложения часто требуют элементов взаимодействия с датами: бронирование встреч, планирование задач, упаковка расписаний и рабочих смен. Календарь — один из самых востребованных компонентов для таких задач. Библиотеки дают готовые решения, но нередко разработчику важно создать кастомный, гибкий календарь под конкретное приложение на Vue.

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

Проектирование структуры календаря

Определение основных элементов

Перед реализацией давайте подумаем, какие функциональные и визуальные элементы нужны для базового отображения календаря:

  • Заголовок месяца/года и кнопки навигации (вперед/назад)
  • Сетка дней недели (Пн, Вт, Ср…)
  • Ячейки дней с правильным смещением для первого дня месяца
  • Возможность выделить текущую дату и выбранную дату
  • Обработка кликов по датам

Для начала спланируем дерево компонентов. Часто календарь реализуют в одном компоненте, но при необходимости его можно разбить:

  • Calendar.vue — основной контейнер календаря
  • CalendarHeader.vue — заголовок с навигацией
  • CalendarGrid.vue — часть с сеткой дней

В этой статье мы сосредоточимся на одном компоненте, чтобы пример был проще для понимания и доработки.

Минимальные требования к данным

Календарю нужны:

  • Текущий выбранный месяц/год
  • Текущая выбранная дата (она же может быть null)
  • Список дней для текущего месяца (включая смещение для первой недели)

Теперь покажу, как всё это реализовать на практике.

Реализация календарного компонента на Vue

Базовая структура компонента

Вот пример скелета, с которого начинается практически любой календарь во Vue 3 (с Options API):

<template>
  <div class="calendar-container">
    <div class="calendar-header">
      <button @click="prevMonth">&#8592;</button>
      <span>{{ monthNames[currentMonth] }} {{ currentYear }}</span>
      <button @click="nextMonth">&#8594;</button>
    </div>
    <div class="calendar-grid">
      <div class="calendar-weekdays">
        <div v-for="wday in weekDays" :key="wday">{{ wday }}</div>
      </div>
      <div class="calendar-days">
        <div
          v-for="(day, idx) in calendarDays"
          :key="idx"
          :class="{
            'not-current-month': !day.currentMonth,
            'today': day.isToday,
            'selected': day.isSelected
          }"
          @click="selectDate(day)"
        >
          {{ day.date.getDate() }}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Calendar',
  data() {
    const today = new Date()
    return {
      currentMonth: today.getMonth(),      // Индекс месяца: 0 - январь
      currentYear: today.getFullYear(),
      selectedDate: null,                  // Хранит выбранную дату
      today: today,
      weekDays: ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'],
      monthNames: [
        'Январь', 'Февраль', 'Март', 'Апрель', 
        'Май', 'Июнь', 'Июль', 'Август', 
        'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
      ]
    }
  },
  computed: {
    calendarDays() {
      return this.generateCalendar()
    }
  },
  methods: {
    // Методы будут описаны позже
  }
}
</script>

В этом коде мы подготовили заготовку для календаря, с которой можно работать дальше. Обратите внимание, что в разметке мы используем циклы для вывода дней и дней недели. Все стили будут зависеть от классов: not-current-month, today, selected.

Генерация дней месяца

Чтобы корректно отобразить сетку месяца, календарю важно:

  • Начать месяц с нужного дня недели (например, если 1 июня — это суббота, то дни должны сдвинуться)
  • Заполнить пустые ячейки предыдущими/следующими месяцами (для целостности сетки)
  • Отметить, какие даты из текущего месяца

Покажу, как реализовать функцию генерации.

methods: {
  generateCalendar() {
    // 1. Определяем первый и последний день месяца
    const firstDayOfMonth = new Date(this.currentYear, this.currentMonth, 1)
    const lastDayOfMonth = new Date(this.currentYear, this.currentMonth + 1, 0)
    // 2. Определяем день недели для первого дня месяца: 0 - воскресенье
    let startDay = firstDayOfMonth.getDay()
    // Для недели, где Пн - первый, превращаем 0 в 6, иначе уменьшаем на 1
    startDay = (startDay + 6) % 7

    const days = []

    // 3. Добавляем предыдущие дни (от конца прошлого месяца, если нужно)
    for (let i = 0; i < startDay; i++) {
      const date = new Date(
        this.currentYear, 
        this.currentMonth, 
        -(startDay - i - 1)
      )
      days.push({
        date,
        currentMonth: false,
        isToday: this.isToday(date),
        isSelected: this.isSelected(date)
      })
    }

    // 4. Текущий месяц
    for (let d = 1; d <= lastDayOfMonth.getDate(); d++) {
      const date = new Date(this.currentYear, this.currentMonth, d)
      days.push({
        date,
        currentMonth: true,
        isToday: this.isToday(date),
        isSelected: this.isSelected(date)
      })
    }

    // 5. Дополняем до полного количества ячеек (6 строк по 7 дней)
    while (days.length % 7 !== 0) {
      const date = new Date(this.currentYear, this.currentMonth + 1, days.length - lastDayOfMonth.getDate() - startDay + 1)
      days.push({
        date,
        currentMonth: false,
        isToday: this.isToday(date),
        isSelected: this.isSelected(date)
      })
    }

    return days
  },
  isToday(date) {
    // Проверяет, совпадает ли дата с сегодняшней
    return (
      date.getDate() === this.today.getDate() &&
      date.getMonth() === this.today.getMonth() &&
      date.getFullYear() === this.today.getFullYear()
    )
  },
  isSelected(date) {
    if (!this.selectedDate) return false
    return (
      date.getDate() === this.selectedDate.getDate() &&
      date.getMonth() === this.selectedDate.getMonth() &&
      date.getFullYear() === this.selectedDate.getFullYear()
    )
  },
  prevMonth() {
    // Переход на предыдущий месяц
    if (this.currentMonth === 0) {
      this.currentMonth = 11
      this.currentYear -= 1
    } else {
      this.currentMonth -= 1
    }
  },
  nextMonth() {
    // Переход на следующий месяц
    if (this.currentMonth === 11) {
      this.currentMonth = 0
      this.currentYear += 1
    } else {
      this.currentMonth += 1
    }
  },
  selectDate(day) {
    if (!day.currentMonth) {
      // Если пользователь кликнул по дню из прошлого/следующего месяца — перелистываем месяц
      const date = day.date
      this.currentMonth = date.getMonth()
      this.currentYear = date.getFullYear()
    }
    this.selectedDate = day.date
  }
}

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

Визуальное выделение дат

Чтобы пользователю было удобно взаимодействовать с вашим календарём, важно визуально показывать текущее положение и выбор:

  • Для выделения сегодняшней даты используйте класс today
  • Для выделенной (выбранной) даты класс selected
  • Для неактивных дат предыдущего или следующего месяца — not-current-month

CSS для этих классов может быть, например, такой:

.calendar-days > div {
  width: 32px;
  height: 32px;
  line-height: 32px;
  text-align: center;
  cursor: pointer;
  border-radius: 50%;
  margin: 2px;
  display: inline-block;
}
.calendar-days > div.today {
  background: #f7b731;
  color: #fff;
}
.calendar-days > div.selected {
  background: #3867d6;
  color: #fff;
}
.calendar-days > div.not-current-month {
  color: #bbb;
  opacity: 0.6;
}
.calendar-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 8px 0;
}
.calendar-weekdays > div {
  font-weight: bold;
  width: 32px;
  text-align: center;
  display: inline-block;
}

Добавьте этот CSS в соответствующие стили компонента или проекта.

Обработка переходов месяца и года

В приведённом выше коде для методов prevMonth и nextMonth реализована корректная обработка смены года при переполнении (например, с декабря на январь, или наоборот). Так обеспечивается плавная навигация по всему диапазону дат.

Выбор даты и связь с моделью данных

В методе selectDate(day) реализован универсальный подход к выбору даты. Выбранная дата сохраняется в состоянии компонента (selectedDate). Если требуется, чтобы календарь работал как контролируемый компонент — передавайте значение через пропсы и сообщайте о выборе с помощью события:

props: {
  modelValue: Date,
},
emits: ['update:modelValue'],
data() {
  // ...
  return {
    // ...
    selectedDate: this.modelValue || null
  }
},
watch: {
  modelValue(newVal) {
    this.selectedDate = newVal
  }
},
methods: {
  selectDate(day) {
    // ...
    this.selectedDate = day.date
    this.$emit('update:modelValue', this.selectedDate)
  }
}

Этот подход позволит использовать календарь с синтаксисом v-model, а родительский компонент будет управлять значением даты.

Кастомизация и расширение календаря

Добавление событий

Календарь часто используется для отображения событий, например встреч, напоминаний или отпусков. Для этого к каждому дню можно прикреплять массив событий и подсвечивать их:

props: {
  events: {
    type: Array,
    default: () => []
  }
},
methods: {
  eventsForDay(date) {
    // Фильтруем события по дате
    return this.events.filter(event => {
      const eventDate = new Date(event.date);
      return (
        date.getDate() === eventDate.getDate() &&
        date.getMonth() === eventDate.getMonth() &&
        date.getFullYear() === eventDate.getFullYear()
      )
    })
  }
}

В шаблоне:

<div
  v-for="(day, idx) in calendarDays"
  :key="idx"
  @click="selectDate(day)"
  :class="..."
>
  {{ day.date.getDate() }}
  <span
    v-if="eventsForDay(day.date).length"
    class="event-dot"
  ></span>
</div>

Добавьте CSS для .event-dot, чтобы события отображались в виде точки или иконки.

Ограничение диапазона дат

Иногда требуется запретить выбор дат вне заданного диапазона (например, нельзя выбрать дату раньше сегодняшнего дня).

Добавьте пропсы minDate и maxDate, а затем проверьте их в методе выбора даты:

methods: {
  canSelect(date) {
    if (this.minDate && date < this.minDate) return false
    if (this.maxDate && date > this.maxDate) return false
    return true
  },
  selectDate(day) {
    if (this.canSelect(day.date)) {
      this.selectedDate = day.date
      this.$emit('update:modelValue', this.selectedDate)
    }
  }
}

В шаблоне используйте условие disabled и измените стили неактивных дней.

Международная локализация и форматы дат

Для поддержки разных языков и региональных стандартов вы можете использовать встроенные методы Intl.DateTimeFormat или сторонние библиотеки (например, date-fns, dayjs, moment.js). Переводите названия месяцев и дней недели, выбирайте правильный первый день недели (например, в США воскресенье, в России — понедельник).

Пример с Intl.DateTimeFormat:

computed: {
  weekDays() {
    // Возвращаем массив дней недели согласно локали
    const formatter = new Intl.DateTimeFormat('ru', { weekday: 'short' })
    const days = []
    for (let i = 1; i <= 7; i++) { // понедельник (1) - воскресенье (7)
      const tempDate = new Date(2021, 0, i)
      days.push(formatter.format(tempDate))
    }
    return days
  }
}

Вы можете передавать локаль как пропс, чтобы поддерживать переключение пользователя.

Поддержка мобильных устройств и адаптивная верстка

Календарь должен оставаться удобным на маленьких экранах. Используйте медиазапросы для уменьшения размера ячеек, увеличения области нажатия и упрощения интерфейса. Можно показать дни одной строкой (горизонтальный скролл) или добавить отдельную мобильную версию через условия в шаблоне.

@media (max-width: 600px) {
  .calendar-days > div,
  .calendar-weekdays > div {
    width: 24px;
    height: 24px;
    line-height: 24px;
    font-size: 12px;
  }
  .calendar-header span {
    font-size: 14px;
  }
}

Выделение диапазона дат (выбор интервала)

Для более сложных задач, например выбора периода заселения в отеле, календарь должен поддерживать выделение диапазона.

Для этого заведите в состоянии два значения: startDate и endDate.

В методе выбора даты реализуйте следующий алгоритм:

  • Если оба значения пусты или заполнены, при выборе первый раз сбрасываете диапазон и устанавливаете только startDate.
  • Если startDate есть, но endDate пустой — сравните значения и определите границы.
  • В шаблоне подсвечивайте дни между startDate и endDate (например, добавьте класс range через computed свойство).

Покажу, как часть этого реализовать:

data() {
  return {
    // ...
    startDate: null,
    endDate: null
  }
},
methods: {
  selectDate(day) {
    if (!this.startDate || (this.startDate && this.endDate)) {
      this.startDate = day.date
      this.endDate = null
    } else {
      if (day.date >= this.startDate) {
        this.endDate = day.date
      } else {
        this.endDate = this.startDate
        this.startDate = day.date
      }
    }
  },
  isInRange(date) {
    if (!this.startDate || !this.endDate) return false
    return date > this.startDate && date < this.endDate
  }
}
// В шаблоне:
:class="{ 'range': isInRange(day.date) }"

Тестирование календаря

Рекомендую протестировать следующие кейсы:

  • Клик по дню меняет выбранную дату
  • Клик по дню из соседнего месяца листает месяц
  • Навигация по месяцам работает в обоих направлениях
  • Ограничения по выбору дат (если заданы)
  • Корректное отображение дат для любого месяца/года
  • Выделение сегодняшнего дня
  • Все функции работают при смене языка/локали

Для автоматических тестов можно использовать Vue Test Utils и JEST.

Заключение

Теперь у вас есть подробная инструкция по реализации кастомного календаря во Vue. Такой компонент легко адаптируется под поиск событий, выбор диапазона, поддержку событий и работу с временными ограничениями. Вы можете расширять функциональность календаря по необходимым сценариям, добавлять стили, поддержку мультивыбора, подсказки, интеграцию с API и многое другое.

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

Частозадаваемые технические вопросы по теме статьи и ответы на них

Как сделать так, чтобы календарь отображал недели с воскресенья?

В функции генерации календаря измените сдвиг startDay для недели, начинающейся с воскресенья:

// С воскресенья: 0 - воскресенье, 1 - понедельник и т.д.
let startDay = firstDayOfMonth.getDay(); // 0 - воскресенье, уже корректно

Также переставьте порядок дней недели в массиве weekDays.


Как добавить поддержку темной темы?

Используйте динамические классы или CSS-переменные для цветов. Например, переключайте класс .dark-theme для контейнера.


Как сделать календарь доступным для скринридеров?

Добавьте атрибуты aria-*, role и семантические теги. Пример: используйте <table> для сетки календаря, добавляйте aria-selected для выбранных дат.


Почему календарь некорректно работает при смене временной зоны?

Везде используйте UTC-методы (getUTCDate, setUTCDate и др.) или всегда приводите локальное время к одной временной зоне, например, через moment.js или date-fns.


Как реализовать выбор нескольких дат (мультиселект)?

Вместо selectedDate делайте массив выбранных дат и изменяйте его по клику на день. Для выделения используйте проверку наличия даты в массиве.

Стрелочка влевоРабота с Canvas во VueРабота с Ant Design Vue для создания UI на VueСтрелочка вправо

Все гайды по Vue

Руководство по валидации форм во Vue.jsИнтеграция Tiptap для создания редакторов на VueРабота с таблицами во Vue через TanStackИнструкция по установке и компонентам Vue sliderУправление пакетами Vue js с помощью npmУправление пакетами и node modules в Vue проектахКак использовать meta для улучшения SEO на VueПолный гайд по компоненту messages во Vuejs5 правил использования Inertia с Vue и LaravelРабота с модулями и пакетами в VueИнструкция по работе с grid на VueGithub для Vue проектов - подробная инструкция по хранению и совместной работеНастройка ESLint для Vue проектов и поддержка качества кодаОбработка ошибок и отладка в Vue.jsИспользование Vue Devtools для отладки и мониторинга приложенийРабота с конфигурационными файлами и скриптами VueСоздание и настройка проектов Vue с помощью Vue CLI3 способа интеграции Chart.js с Vue для создания графиковРабота с Canvas во VueИнструкция по реализации календаря во VueРабота с Ant Design Vue для создания UI на Vue
Обзор и использование утилит Vue для удобной разработкиРабота с обновлениями компонента и жизненным циклом updateРазрешение конфликтов и ошибок с помощью Vue resolveИспользование query-параметров и их обработка в маршрутах VueЗагрузка и управление состоянием загрузки в VueИспользование библиотек Vue для расширения функционалаРабота с JSON данными в приложениях VueКак работать с экземплярами компонента Instance во VueПолучение данных и API-запросы во Vue.jsЭкспорт и импорт данных и компонентов в VueОбработка событий и их передача между компонентами VuejsГайд по defineEmits на Vue 3Понимание core функционала Vue и его применениеПонимание и применение Composition API в Vue 3Понимание и работа с компилятором VueКогда и как использовать $emit и call во VueВзаимодействие с внешними API через Axios в Vue
Веб приложения на Vue архитектура и лучшие практикиИспользование Vite для быстрого старта и сборки проектов на Vue 3Работа с URL и ссылками в приложениях на VueРабота с пользовательскими интерфейсами и UI библиотеками во VueОрганизация и структура исходных файлов в проектах VueИспользование Quasar Framework для разработки на Vue с готовыми UI-компонентамиОбзор популярных шаблонов и стартовых проектов на VueИнтеграция Vue с PHP для создания динамичных веб-приложенийКак организовать страницы и маршруты в проекте на VueNuxt JS и Vue 3 для SSR приложенийСоздание серверных приложений на Vue с помощью Nuxt jsИспользование Vue Native для разработки мобильных приложенийОрганизация и управление индексной страницей в проектах VueИспользование Docker для контейнеризации приложений на VueИнтеграция Vue.js с Django для создания полноценных веб-приложенийСоздание и работа с дистрибутивом build dist Vue приложенийРабота со стилями и CSS в Vue js для красивых интерфейсовСоздание и структурирование Vue.js приложенияКак исправить ошибку cannot find module vueНастройка и сборка проектов Vue с использованием современных инструментовИнтеграция Vue с Bitrix для корпоративных решенийРазработка административных панелей на Vue js
5 библиотек для создания tree view во VueИнтеграция Tailwind CSS с Vue для современных интерфейсовИнтеграция Vue с серверной частью и HTTPS настройкамиКак обрабатывать async операции с Promise во VueИнтеграция Node.js и Vue.js для разработки приложенийРуководство по интеграции Vue js в NET проектыПримеры использования JSX во VueГайд по импорту и регистрации компонентов на VueМногоязычные приложения на Vue с i18nИнтеграция FLIR данных с Vue5 примеров использования filter во Vue для упрощения разработки3 примера реализации drag-and-drop во Vue
Управление переменными и реактивными свойствами во VueИспользование v for и slot в VueПрименение v-bind для динамической привязки атрибутов в VueУправление пользователями и их данными в Vue приложенияхСоздание и использование UI Kit для Vue приложенийТипизация и использование TypeScript в VuejsИспользование шаблонов в Vue js для построения интерфейсовИспользование Swiper для создания слайдеров в VueРабота со стилями и стилизацией в VueСтруктура и особенности Single File Components SFC в VueРабота со SCSS в проектах на Vue для стилизацииРабота со скроллингом и прокруткой в Vue приложенияхПрименение script setup синтаксиса в Vue 3 для упрощения компонентовИспользование scoped стилей для изоляции CSS в компонентах Vue3 способа улучшить навигацию Vue с push()Обработка запросов и асинхронных операций в VueПонимание и использование provide inject для передачи данных между компонентамиПередача и использование props в Vue 3 для взаимодействия компонентовПередача данных между компонентами с помощью props в Vue jsУправление property и функциями во Vue.jsРабота со свойствами компонентов VueУправление параметрами и динамическими данными во VueРабота с lifecycle-хуком onMounted во VueОсновы работы с объектами в VueПонимание жизненного цикла компонента Vue js на примере mountedИспользование модальных окон modal в Vue приложенияхИспользование методов в компонентах Vue для обработки логикиИспользование метода map в Vue для обработки массивовИспользование хуков жизненного цикла Vue для управления состоянием компонентаРабота с ключами key в списках и компонентах VueОбработка пользовательского ввода в Vue.jsРабота с изображениями и их оптимизация в VueИспользование хуков жизненного цикла в VueОрганизация сеток и гридов для верстки интерфейсов на VueСоздание и управление формами в VueОрганизация файлов и структура проекта Vue.jsКомпоненты Vue создание передача данных события и emitРабота с динамическими компонентами и данными в Vue3 способа манипулирования DOM на VueРуководство по div во VueИспользование директив в Vue и их расширенные возможностиОсновы и применение директив в VueИспользование директив и их особенности на Vue с помощью defineИспользование компонентов datepicker в Vue для выбора датОрганизация циклов и итераций во VueКак работает компиляция Vue CoreСоздание и использование компонентов в Vue JSОбработка кликов и пользовательских событий в VueИспользование классов в Vue для организации кода и компонентовИспользование директивы checked для управления состоянием чекбоксов в VueГайд на checkbox компонент во VueОтображение данных в виде графиков с помощью Vue ChartСоздание и настройка кнопок в VueСоздание и настройка кнопок в Vue приложенияхРабота с lifecycle-хуками beforeCreate и beforeMount во VueИспользование массивов и методов их обработки в VueИспользование массивов и их обработка в Vue
Использование Vuetify для создания современных интерфейсов на VueИспользование transition во VueТестирование компонентов и приложений на VueРабота с teleport для управления DOM во VueПять шагов по настройке SSR в VuejsИспользование Shadcn UI компонентов с Vue для продвинутых интерфейсовИспользование router-link для навигации в Vue RouterКак использовать require в Vue для динамического импорта модулейРабота с динамическим рендерингом и виртуальным DOM на Vue.jsИспользование ref для управления ссылками и реактивностью в Vue 3Использование Vue Pro и его преимущества для профессиональной разработкиРуководство по nextTick для работы с DOMСоздание и использование компонентов с помощью Vue js и CУправление состоянием и реактивностью через inject и provideДинамическое обновление компонентов и данных на VueГлубокое изучение документации Vue и как эффективно её использоватьИспользование Crystal с Vue для разработкиИспользование вычисляемых свойств для динамического отображения данных на Vue jsОптимизация производительности и предупреждения в Vue
Открыть базу знаний