Skip to content

Month Calendar

A complete month calendar implementation using useTemporal.

Stable Month Grid

For a consistent 6-week (42-day) calendar grid, check out the stableMonth unit in the @usetemporal/calendar-units package. It provides a cleaner solution than the manual implementation shown below.

Basic Month Calendar

vue
<template>
  <div class="calendar">
    <div class="calendar-header">
      <button @click="previousMonth">‹</button>
      <h2>{{ monthName }}</h2>
      <button @click="nextMonth">›</button>
    </div>
    
    <div class="calendar-grid">
      <div v-for="day in weekDays" :key="day" class="weekday">
        {{ day }}
      </div>
      
      <div 
        v-for="day in days" 
        :key="day.date.toISOString()"
        :class="getDayClasses(day)"
        @click="selectDay(day)"
      >
        {{ day.date.getDate() }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { 
  usePeriod, 
  divide, 
  next, 
  previous,
  isSame,
  isWeekend,
  isToday
} from 'usetemporal'

const props = defineProps({
  temporal: {
    type: Object,
    required: true
  }
})

// Reactive month period
const month = usePeriod(props.temporal, 'month')

// Month name for header
const monthName = computed(() => 
  month.value.date.toLocaleDateString('en', { 
    month: 'long', 
    year: 'numeric' 
  })
)

// Days in the month
const days = computed(() => 
  divide(props.temporal, month.value, 'day')
)

// Weekday names
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']

// Navigation
const previousMonth = () => {
  props.temporal.browsing.value = previous(props.temporal, month.value)
}

const nextMonth = () => {
  props.temporal.browsing.value = next(props.temporal, month.value)
}

// Day selection
const selectedDay = ref(null)
const selectDay = (day) => {
  selectedDay.value = day
}

// Day styling
const getDayClasses = (day) => ({
  'calendar-day': true,
  'weekend': isWeekend(day),
  'today': isToday(day, props.temporal),
  'selected': selectedDay.value && 
    isSame(props.temporal, day, selectedDay.value, 'day')
})
</script>

<style>
.calendar {
  width: 350px;
  font-family: system-ui;
}

.calendar-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1rem;
}

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}

.weekday {
  text-align: center;
  font-weight: bold;
  padding: 0.5rem;
  font-size: 0.875rem;
}

.calendar-day {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  border-radius: 4px;
  transition: all 0.2s;
}

.calendar-day:hover {
  background-color: #f0f0f0;
}

.weekend {
  color: #666;
}

.today {
  background-color: #e3f2fd;
  font-weight: bold;
}

.selected {
  background-color: #1976d2;
  color: white;
}
</style>

Stable Month Calendar (6-week grid)

For a consistent 6-week calendar grid that always shows the same number of cells:

Install the calendar units package:

bash
npm install @usetemporal/calendar-units

Then use the stableMonth unit:

vue
<template>
  <div class="stable-calendar">
    <div class="calendar-header">
      <button @click="previousMonth">‹</button>
      <h2>{{ monthName }}</h2>
      <button @click="nextMonth">›</button>
    </div>
    
    <div class="calendar-grid">
      <div 
        v-for="day in days" 
        :key="day.date.toISOString()"
        :class="getDayClasses(day)"
      >
        {{ day.date.getDate() }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { period, divide, next, previous } from 'usetemporal'
import '@usetemporal/calendar-units' // Registers stableMonth unit

const props = defineProps({
  temporal: Object
})

// Create stable month (always 42 days)
const stableMonth = computed(() => 
  period(props.temporal, 'stableMonth', props.temporal.browsing.value)
)

// Get all 42 days
const days = computed(() => 
  divide(props.temporal, stableMonth.value, 'day')
)

const monthName = computed(() => 
  props.temporal.browsing.value.date.toLocaleDateString('en', { 
    month: 'long', 
    year: 'numeric' 
  })
)

const getDayClasses = (day) => ({
  'calendar-day': true,
  'current-month': day.date.getMonth() === props.temporal.browsing.value.date.getMonth(),
  'other-month': day.date.getMonth() !== props.temporal.browsing.value.date.getMonth()
})

const previousMonth = () => {
  props.temporal.browsing.value = previous(props.temporal, props.temporal.browsing.value)
}

const nextMonth = () => {
  props.temporal.browsing.value = next(props.temporal, props.temporal.browsing.value)
}
</script>

Manual Implementation (Legacy)

vue
<template>
  <div class="stable-calendar">
    <!-- Same header as above -->
    
    <div class="calendar-grid">
      <div 
        v-for="day in stableDays" 
        :key="day.date.toISOString()"
        :class="getStableDayClasses(day)"
      >
        {{ day.date.getDate() }}
      </div>
    </div>
  </div>
</template>

<script setup>
// ... imports

// Generate 6-week calendar grid
const stableDays = computed(() => {
  const monthPeriod = month.value
  const weeks = divide(props.temporal, monthPeriod, 'week')
  
  // Get all weeks that touch this month
  const firstWeek = weeks[0]
  const prevWeek = previous(props.temporal, firstWeek)
  const allWeeks = [prevWeek, ...weeks]
  
  // Ensure 6 weeks total
  while (allWeeks.length < 6) {
    const lastWeek = allWeeks[allWeeks.length - 1]
    allWeeks.push(next(props.temporal, lastWeek))
  }
  
  // Get all days from the weeks
  return allWeeks.flatMap(week => divide(props.temporal, week, 'day'))
})

const getStableDayClasses = (day) => ({
  'calendar-day': true,
  'weekend': isWeekend(day),
  'today': isToday(day, props.temporal),
  'other-month': !isSame(props.temporal, day, month.value, 'month'),
  'selected': selectedDay.value && 
    isSame(props.temporal, day, selectedDay.value, 'day')
})
</script>

<style>
.other-month {
  opacity: 0.3;
}
</style>

With Events

Add event display to your calendar:

vue
<template>
  <div class="calendar-with-events">
    <!-- Calendar grid -->
    <div 
      v-for="day in days" 
      :key="day.date.toISOString()"
      class="calendar-day"
    >
      <div class="day-number">{{ day.date.getDate() }}</div>
      
      <div class="day-events">
        <div 
          v-for="event in getEventsForDay(day)"
          :key="event.id"
          class="event-dot"
          :style="{ backgroundColor: event.color }"
        />
      </div>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  temporal: Object,
  events: Array // [{ id, date, title, color }]
})

const getEventsForDay = (day) => {
  return props.events.filter(event => 
    isSame(props.temporal, 
      toPeriod(props.temporal, event.date, 'day'),
      day,
      'day'
    )
  )
}
</script>

Usage

vue
<template>
  <MonthCalendar :temporal="temporal" />
</template>

<script setup>
import { createTemporal } from 'usetemporal'
import MonthCalendar from './MonthCalendar.vue'

const temporal = createTemporal({ 
  date: new Date(),
  weekStartsOn: 1 // Monday
})
</script>

See Also

Released under the MIT License.