DEV Community

Marco
Marco

Posted on

Calendar view with vue3 and tailwind

As JS libraries and frameworks keeps churning, I find myself looking for libraries that are actively maintained. UI libraries tend to be forgotten and end up unmaintained or worse they break when a large update is published. That's why, when I can, I build my own custom components and keep it as simple as humanly possible. Calendar components are a good example of a lib I struggle to find one that works. Every project has its own requirements and using an out of the box UI rarely works.

With CSS grid building your own is now fairly easy so the following is a quick skeleton calendar, customizable to fit your needs.

Calendar View

To get started you'll need

  • Vue3
  • Tailwind
  • dayjs

We start with an empty vue component

<template>
</template>

<script setup lang='ts'>
</script>
Enter fullscreen mode Exit fullscreen mode

Let's define some properties. We'll need a modelValue prop to feed in events and a startDate that sets the initial month to display.

<script setup lang='ts'>

type Props = {
    modelValue?: any
    startDate?: string    
}
const props = withDefaults(defineProps<Props>(), {
    modelValue: () => null,    
    startDate: () => '2022-12-05'
});
const emits = defineEmits(['update:modelValue']);
</script>
Enter fullscreen mode Exit fullscreen mode

Next comes a function to calculates the day in a given month. Dayjs has all the functions to work that out, so let's import.

Adding the function as a computed property will make it reactive. Every time viewDate changes, the range is recalculated.

<script setup lang='ts'>
import { computed, ref } from 'vue';
import dayjs from 'dayjs';

const viewDate = ref(dayjs(props.startDate));

const units = computed(() => {
    let ranges = [];
    let startOfRange = viewDate.value.startOf('month').add(-1,'day');
    let endOfRange = viewDate.value.endOf('month').add(-1,'day');

    let currentDate = startOfRange;

    while (currentDate.isBefore(endOfRange) || currentDate.isSame(endOfRange)) {
        currentDate = currentDate.add(1, 'day');
        ranges.push(currentDate);
    }
    return ranges;
})

</script>

Enter fullscreen mode Exit fullscreen mode

Now Let's add a css grid to show our calendar boxes

<template>  
    <div class="grid grid-cols-7">        
        <div class="border border-slate-200 flex flex-col h-32"
             v-for="d in units">
            <div class="text-center">{{ d.format('D') }}</div>      
        </div>
    </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Most calendars, show the days of the week at the very top. To make that work, we'll need to align the 1st day of the month with the correct day.
So we'll need a second function to calculate that gap

const daystoPrepend = computed(() => {
    const startOfMonth = viewDate.value.startOf("month");
    const startOfFirstWeek = startOfMonth.startOf("week");
    const daysToFirstDay = startOfMonth.diff(startOfFirstWeek, "day");
    return Array.from(new Array(daysToFirstDay).keys());
})
Enter fullscreen mode Exit fullscreen mode

and let's add empty boxes in front to align our days.

    <div class="grid grid-cols-7">
        <div v-for="p in daystoPrepend"></div>
        <div class="border border-slate-200 flex flex-col h-32"
             v-for="d in units">
            <div class="text-center">{{ d.format('D') }}</div>           
        </div>
    </div>
Enter fullscreen mode Exit fullscreen mode

Now we can add the days on top of the calendar.

<template>
    <div class="grid grid-cols-7 gap-1">
        <div v-for="d in weekDays"
             class="text-center">
            <div>{{ d }}</div>
        </div>
    </div>

    <div class="grid grid-cols-7">
        <div v-for="p in daystoPrepend"></div>
        <div class="border border-slate-200 flex flex-col h-32"
             v-for="d in units">
            <div class="text-center">{{ d.format('D') }}</div>      
        </div>
    </div>
</template>

const weekDays = [
    'Sunday',
    'Monday',
    'Tuesday',
    'Wednesday',
    'Thursday',
    'Friday',
    'Saturday',
]

</script>
Enter fullscreen mode Exit fullscreen mode

Finally we'll need some buttons to navigate between months and reset to today. Since everything is computed from viewDate we just need to manipulate that value.

<div class="flex">

    <button class="btn-primary"
            @click="reset()">Today</button>
    <button class="btn"
            @click="shiftMonth(-1)">Previous</button>
    <button class="btn"
            @click="shiftMonth(1)">Next</button>
    <span class="text-3xl">{{ viewDate.format('MMMM YYYY') }}</span>

</div>

const shiftMonth = function (amount: number) {
    viewDate.value = viewDate.value.add(amount, 'month');
}
const reset = function () {
    viewDate.value = dayjs();
}

</script>
Enter fullscreen mode Exit fullscreen mode

and we're done 😊 pass your content in modelValue, render them and 💰

Here is the full code for reference

Top comments (1)

Collapse
 
hamhamfonfon profile image
Stéphane Méaudre

Very interesting, thank you :). Would it be complicated to modify to add events on a day or a date range?