Building a Custom Weekly Calendar Component in Vue.js

Requirement Analysis

In certain mobile application scenarios, displaying a full monthlly calendar view consumes excessive vertical space. A weekly strip offers a more compact alternative while maintaining necessary date selection functionality. The following implementation outlines a reusable Vue compnoent designed to display a seven-day window with navigation capabilities.

Component Implemantation

Create a new file named WeeklyCalendar.vue. This component utilizes the Vue 3 Composition API for state management and lifecycle handling. It includes a header for navigation, a row for weekday labels, and a row for selectable dates.

<template>
  <div class="weekly-calendar">
    <header class="calendar-nav">
      <span class="date-label">{{ displayYear }}/{{ displayMonth }}</span>
      <div class="controls">
        <svg class="icon-btn" @click="navigatePrevious" viewBox="0 0 24 24">
          <path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none"/>
        </svg>
        <svg class="icon-btn" @click="navigateNext" viewBox="0 0 24 24">
          <path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" fill="none"/>
        </svg>
      </div>
    </header>
    <div class="calendar-body">
      <div class="weekdays">
        <span v-for="(day, idx) in weekDayLabels" :key="idx" class="weekday-label">
          {{ day }}
        </span>
      </div>
      <div class="dates">
        <span 
          v-for="(item, idx) in currentWeekDates" 
          :key="idx" 
          class="date-item"
          :class="{ active: selectedTimestamp === item.timestamp, isToday: todayTimestamp === item.timestamp }"
          @click="handleDateSelection(item)"
        >
          {{ todayTimestamp === item.timestamp ? 'Today' : item.dayNumber }}
        </span>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue';

const WEEK_DAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];

const state = reactive({
  displayYear: '',
  displayMonth: '',
  todayTimestamp: 0,
  selectedTimestamp: 0,
  weekDayLabels: WEEK_DAY_LABELS,
  currentWeekDates: []
});

const { displayYear, displayMonth, todayTimestamp, selectedTimestamp, weekDayLabels, currentWeekDates } = state;

const emit = defineEmits(['date-change']);

onMounted(() => {
  initializeCalendar();
});

const initializeCalendar = () => {
  const now = new Date();
  const nowTime = now.getTime();
  
  updateDateDisplay(nowTime);
  
  state.todayTimestamp = nowTime;
  state.selectedTimestamp = nowTime;

  // Calculate start of week (Monday)
  const dayOfWeek = now.getDay(); 
  const diffToMonday = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
  const mondayTime = new Date(now.setDate(diffToMonday)).getTime();
  
  generateWeekDays(mondayTime);
  
  // Trigger initial selection
  handleDateSelection({
    timestamp: state.todayTimestamp,
    dayNumber: new Date(state.todayTimestamp).getDate()
  });
};

const updateDateDisplay = (time) => {
  const date = new Date(time);
  state.displayYear = date.getFullYear();
  const month = date.getMonth() + 1;
  state.displayMonth = month < 10 ? `0${month}` : `${month}`;
};

const generateWeekDays = (startTimestamp) => {
  updateDateDisplay(startTimestamp);
  const weekData = [];
  
  for (let i = 0; i < 7; i++) {
    const currentDayTime = startTimestamp + (i * 24 * 60 * 60 * 1000);
    const dateObj = new Date(currentDayTime);
    weekData.push({
      timestamp: currentDayTime,
      dayNumber: dateObj.getDate()
    });
  }
  state.currentWeekDates = weekData;
};

const navigatePrevious = () => {
  if (state.currentWeekDates.length > 0) {
    const prevStart = state.currentWeekDates[0].timestamp - (7 * 24 * 60 * 60 * 1000);
    generateWeekDays(prevStart);
  }
};

const navigateNext = () => {
  if (state.currentWeekDates.length > 0) {
    const nextStart = state.currentWeekDates[0].timestamp + (7 * 24 * 60 * 60 * 1000);
    generateWeekDays(nextStart);
  }
};

const handleDateSelection = (item) => {
  updateDateDisplay(item.timestamp);
  state.selectedTimestamp = item.timestamp;
  
  const day = item.dayNumber < 10 ? `0${item.dayNumber}` : item.dayNumber;
  const formattedDate = `${state.displayYear}-${state.displayMonth}-${day}`;
  
  emit('date-change', formattedDate);
};
</script>

<style scoped lang="less">
.weekly-calendar {
  font-size: 14px;
  background: #fff;
  color: #666;
  padding: 12px;
  border-radius: 8px;
  
  .calendar-nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
    color: #333;
    font-weight: 600;
    font-size: 16px;
    
    .controls {
      display: flex;
      gap: 12px;
      
      .icon-btn {
        width: 20px;
        height: 20px;
        cursor: pointer;
        &:active {
          opacity: 0.6;
        }
      }
    }
  }
  
  .calendar-body {
    .weekdays, .dates {
      display: flex;
      justify-content: space-between;
      text-align: center;
      
      span {
        flex: 1;
      }
    }
    
    .weekdays {
      color: #333;
      font-size: 15px;
      margin-bottom: 8px;
    }
    
    .dates {
      .date-item {
        color: #999;
        padding: 8px 0;
        cursor: pointer;
        transition: color 0.2s;
        
        &.isToday {
          color: #333;
          font-weight: 600;
        }
        
        &.active {
          color: #e74c3c;
          font-weight: bold;
          background: rgba(231, 76, 60, 0.1);
          border-radius: 4px;
        }
      }
    }
  }
}
</style>

Integration Guide

Import the component into the parent view and listen for the date-change event to retrieve the selected date string.

<template>
  <div class="app-container">
    <WeeklyCalendar @date-change="processSelectedDate" />
  </div>
</template>

<script setup>
import WeeklyCalendar from './components/WeeklyCalendar.vue';

const processSelectedDate = (dateString) => {
  console.log('Selected Date:', dateString);
  // Proceed with API calls or logic using dateString (YYYY-MM-DD)
};
</script>

Tags: vuejs composition-api calendar-component frontend-development javascript

Posted on Fri, 11 Sep 2026 16:11:25 +0000 by russia5