Blog
Jan 25, 2022 - 6 MIN READ
Building a Date Picker

Building a Date Picker

Continuing to work on my rebuild of the GPX app, I decided to build my own datepicker using Vue 3, Bootstrap 5, and TypeScript

Steve Furches

Steve Furches

Final Code on GitHub: github.com/MSF42/VueDatePicker

GPX App

The GPX app is coming along quite well. I've learned a lot about TypeScript and working with dates in JavaScript. My next blog post will be an update on that. For now, I just want to talk about the datepicker I built.

Datepickers

There are a lot of datepickers to choose from out there. When I needed one for this app, I tried three or four from the vast selection on npm. Out of the few that were aesthetically pleasing, putting them to use required more effort than I cared to expend. So, I decided to build my own! I thought it would be a nice learning opportunity. Developers often complain about working with dates in JavaScript, so I thought this would be a great chance to get into it.

Problem Solving

First, the basics:

Getting and Emitting Date

I needed the datepicker to be a reusable component, of course. So I set it up to receive the date from the parent and emit the chosen date back to the parent.

  props: {
    date: {
      required: true,
      type: Date,
    },
  },
  emits: ["selected-date"],

Basic Info

const { date } = toRefs(props);

// GET THE YEAR, MONTH, AND DAY FOR DROPDOWN DISPLAY
const dateYear = computed(() => {
  return date.value.getFullYear();
});
// A NUMBER VERSION OF THE MONTH TO WORK WITH
const dateMonthInt = computed(() => {
  return date.value.getMonth();
});
// A STRING VERSION OF THE MONTH TO DISPLAY
const dateMonthStr = computed(() => {
  let month = months[dateMonthInt.value];
  return month.length === 4
    ? month
    : months[dateMonthInt.value].slice(0, 3);
});
// ADD A "0" IF THE DATE IS LESS THAN 10
const dateDate = computed(() => {
  return ((date.value.getDate() < 10 ? "0" : "") +
    date.value.getDate()) as unknown as number;
});

// MONTH AND DAY LISTS
const months = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
];
const days = ["Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"];

Toggle Calendar

const showDate = ref(false);
const toggleDateView = () => {
    showDate.value = !showDate.value;
};

Emit the Chosen Date

Letting the parent know the date has been chosen:

const dateChosen = (row: number, col: number) => {
  let newdate = monthArray.value[row][col]
  context.emit('selected-date', newdate)
}

Time Zone Offset

If I don't use a timezone offset, the chosen date and displayed date can get out of sync. Adding this offset effectively sets the time for each day to midnight.

// add this to all new dates (milliseconds)
const tzOffset = computed(() => {
    return date.value.getTimezoneOffset() * 1000;
});

Month Length and First Day of Month

Since JavaScript will return February 1 if you pass in January 32, this creates a handy way to calculate the length of a month: Simply subtract the date value from 32.

Example: January 32 will return February 1, so 32 - 1 = 31

const getMonthLength = computed(() => {
    return 32 - new Date(dateYear.value, dateMonthInt.value, 32).getDate();
});

// GET THE FIRST DAY OF THE MONTH
const getMonthFirstDay = computed(() => {
    let weekStartsOnSun = new Date(dateYear.value, dateMonthInt.value).getDay();
    return weekStartsOnSun === 0 ? 6 : weekStartsOnSun - 1;
    // I subtracted 1 so the week could start on Monday
});

Month Array (this was a bit tricky)

  1. I created an array of 42 dates, since some months will need to display six weeks.
  2. I took the first day of the month and subtracted the 'day' value in order to find out what date needed to be in the first square. For example, if February 1 is a Thursday, the 'day' value for Thursday is 3. The first column (Monday) will start on February 1st - 3, or January 29.
  3. I used 86400000 to account for the number of milliseconds in a day.
  4. I added a day for each item in the array.
  5. Finally, I returned six slices of the array, representing each week. This results in an array of six arrays (i.e. 'weeks').
const monthArray = computed(() => {
    let tempArray = [];
    let firstDay = new Date(dateYear.value, dateMonthInt.value);
    for (let i = 0; i < 42; i++) {
        tempArray.push(
            new Date(
                firstDay.getTime() +
                    i * 86400000 +
                    tzOffset.value -
                    getMonthFirstDay.value * 86400000,
            ),
        );
    }
    return [
        tempArray.slice(0, 7),
        tempArray.slice(7, 14),
        tempArray.slice(14, 21),
        tempArray.slice(21, 28),
        tempArray.slice(28, 35),
        tempArray.slice(35),
    ];
});

Advancing the Calendar

I made a single function to move forward/backward by one month or year. If moving back one month, "0" is passed in for the year (no change) and "-1" is passed in for the month.

One issue I didn't think about initially - if the selected date is January 31, for example, and the user advanced one month, the calendar advanced to March 3 (i.e. "February 31"). Therefore I had to set up a month length check. Now this example will result in advancing to February 28 (or 29).

const dateChange = (year: number, month: number) => {
  let tempdate
  // CHECK LENGTH OF THE MONTH WE ARE ADVANCING TO
  let newMonthLength =
    32 - new Date(dateYear.value, dateMonthInt.value + month, 32).getDate()
  if (year === 0 && dateDate.value > newMonthLength) {
    tempdate = new Date(
      new Date(
        dateYear.value,
        dateMonthInt.value + month,
        newMonthLength
      ).getTime() + tzOffset.value
    )
  } else {
    tempdate = new Date(
      new Date(
        dateYear.value + year,
        dateMonthInt.value + month,
        dateDate.value
      ).getTime() + tzOffset.value
    )
  }
  context.emit('selected-date', tempdate)
}

Highlighting Dates

// HIGHLIGHT THE SELECTED DATE IN RED
const isSelectedDate = (date1: Date, date2: Date) => {
  return date1.getFullYear() === date2.getFullYear() &&
    date1.getMonth() === date2.getMonth() &&
    date1.getDate() === date2.getDate()
    ? 'bg-red-500 text-bold text-gray-200'
    : ''
}

// HIGHLIGHT TODAY'S DATE IN BLUE
const isCurrentDate = (date: Date) => {
  let today = new Date(Date.now())
  return date.getFullYear() === today.getFullYear() &&
    date.getMonth() === today.getMonth() &&
    date.getDate() === today.getDate()
    ? 'bg-blue-500 text-bold text-gray-200'
    : ''
}

// MAKE DAYS OF CURRENTLY VIEWED MONTH BOLD, MAKE OTHERS LIGHTER
const isNotCurrentMonth = (date1: Date, date2: Date) => {
  return date1.getFullYear() === date2.getFullYear() &&
    date1.getMonth() === date2.getMonth()
    ? 'text-bold'
    : 'text-gray-500'
}

Lastly, a Directive to Highlight on Hover

directives: {
  hover: {
    beforeMount(el, binding) {
      const { value = [] } = binding;
      el.addEventListener("mouseenter", () => {
        el.classList.add(...value);
      });
      el.addEventListener("mouseleave", () => {
        el.classList.remove(...value);
      });
    },
    unmounted(el, binding) {
      el.removeEventListener("mouseenter", binding);
      el.removeEventListener("mouseleave", binding);
    },
  },
},
////// and in the element above
v-hover="['bg-blue-700', 'text-gray-300']"

Summary

I really enjoyed this little challenge! It was great practice in several areas - directives, JavaScript date functions, and TypeScript.

Perhaps someone else will find a use for my datepicker!

Built with Nuxt UI • © 2026 Steve Furches