Calendar component example with date-fns (Vue 3)
Tags: calendar, vue, vue3, tailwindcss
Published: 2022-09-11 15:48:39

---
title: Calendar component example with date-fns (Vue 3)
published: true
description: 
tags: #calendar #vue #vue3 #tailwind
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
---
Sometimes we need to implement a custom calendar view and with date-fns it's not as scary as you may think.

First, let's see some code:
```js
const currentDate = ref(startOfDay(new Date()));

const days = computed(() => {
  const monthStart = startOfMonth(currentDate.value);
  const dayNumInWeek = getDay(monthStart);

  const calendarStart = subDays(monthStart, dayNumInWeek !== 0 ? dayNumInWeek - 1 : 6);

  return eachDayOfInterval({
    start: calendarStart,
    end: addDays(calendarStart, 41)
  }).map((date) => {{
    return {
      isCurrent: compareAsc(currentDate.value, date) === 0,
      isCurrentMonth: date.getMonth() === currentDate.value.getMonth(),
      date: date
    }
  }});
});
```
It's not hard to understand and looks pretty simple. But if you do not understand it immediately let's explain it step by step:

Firstly we need to get the start and end of the current month:
```js
const monthStart = startOfMonth(currentDate.value);
const monthEnd = endOfMonth(currentDate.value);
```
Our **monthStart** and **monthEnd** is Date objects contains date of start and end date of the month. So we can get which day the current month is start:
```js
const dayNumInWeek = getDay(monthStart)
```
And now we can use it to get the start date for our calendar:
```js
const calendarStart = subDays(monthStart, dayNumInWeek !== 0 ? dayNumInWeek - 1 : 6);
```
We need it because we display 7 weeks' days in our calendar and the first day of the current month can be on any day of the week. Sometimes a day first day of the month in the week context can be 0(Sunday) and we need to sub 6 days if it happens.

Finally, we need to get an interval between **calendarStart** and **calendarStart + 41** because we want to display 42 days per time and map it into a useful object:
```js
eachDayOfInterval({
    start: calendarStart,
    end: addDays(calendarStart, 41)
  }).map((date) => {{
    return {
      isCurrent: compareAsc(currentDate.value, date) === 0,
      isCurrentMonth: date.getMonth() === currentDate.value.getMonth(),
      date: date
    }
  }});

```
Finally, let's implement the full example code with Vue 3 and Tailwind:

```vue
<template>
  <div class="w-60 bg-white  dark:border-[1px] dark:border-gray_border dark:bg-gray_800 rounded-lg shadow-lg">
    <div class="flex justify-center space-x-2 py-3 items-center rounded-lg">
      <button
          type="button"
          class="-my-1.5 flex flex-none items-center justify-center p-1.5"
          @click="toPreviousMonth"
      >
        <span class="sr-only">Previous month</span>
        <!-- Heroicon name: solid/chevron-left -->
        <svg class="h-5 w-5 icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
             aria-hidden="true">
          <path fill-rule="evenodd"
                d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
                clip-rule="evenodd"/>
        </svg>
      </button>
      <h2 class="font-normal text-gray_800 text-sm dark:text-white font-inter">
        {{ format(currentDate, 'MMMM yyyy') }}
      </h2>
      <button
          type="button"
          class="-my-1.5 -mr-1.5 ml-2 flex flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500"
          @click="toNextMonth"
      >
        <!-- Heroicon name: solid/chevron-right -->
        <svg class="h-5 w-5 icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
             aria-hidden="true">
          <path fill-rule="evenodd"
                d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
                clip-rule="evenodd"/>
        </svg>
      </button>
    </div>
    <div class="mt-2 grid grid-cols-7 text-center text-sm">
      <div class="text-gray_500 font-inter dark:text-white">Mo</div>
      <div class="text-gray_500 font-inter dark:text-white">Tu</div>
      <div class="text-gray_500 font-inter dark:text-white">We</div>
      <div class="text-gray_500 font-inter dark:text-white">Th</div>
      <div class="text-gray_500 font-inter dark:text-white">Fr</div>
      <div class="text-gray_500 font-inter dark:text-white">Sa</div>
      <div class="text-gray_500 font-inter dark:text-white">Su</div>
    </div>
    <div class="mt-2 grid grid-cols-7 text-sm font-inter">
      <button
          v-for="(day, index) in days" :key="`day-${index}`"
          type="button"
          class="mx-auto flex h-8 w-8 items-center justify-center rounded-lg"
          @click="currentDate = day.date"
          :class="{'bg-blue_400 justify-center rounded-lg': day.isCurrent}"
      >
        <time :class="{
          'text-number_calendar': !day.isCurrentMonth,
          'text-gray_800 dark:text-white': day.isCurrentMonth
        }">
          {{ format(day.date, 'd') }}
        </time>
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import {
  subDays,
  addDays,
  eachDayOfInterval,
  format,
  startOfMonth,
  compareAsc,
  startOfDay,
  addMonths,
  subMonths,
  getDay,
} from 'date-fns'
import {computed, ref} from "vue";

const currentDate = ref(startOfDay(new Date()));

const days = computed(() => {
  const monthStart = startOfMonth(currentDate.value);
  const dayNumInWeek = getDay(monthStart);

  const calendarStart = subDays(monthStart, dayNumInWeek !== 0 ? dayNumInWeek - 1 : 6);

  return eachDayOfInterval({
    start: calendarStart,
    end: addDays(calendarStart, 41)
  }).map((date) => {{
    return {
      isCurrent: compareAsc(currentDate.value, date) === 0,
      isCurrentMonth: date.getMonth() === currentDate.value.getMonth(),
      date: date
    }
  }});
});

function toNextMonth() {
  currentDate.value = addMonths(startOfMonth(currentDate.value), 1)
}

function toPreviousMonth() {
  currentDate.value = subMonths(startOfMonth(currentDate.value), 1)
}
</script>
```