Building a Real-Data Monthly Calendar in Vanilla JavaScript

Calendars are a staple of web UIs, and while reaching for an embedded Google Calendar or plugin is common, building one from scratch is more approachable than it seems. With just HTML, CSS, and a small utility library, you can render a fully interactive, data-driven monthly grid. Let's walk through the process.

A working demo is available on CodeSandbox if you want to follow along with the final result.

Before writing any code, it helps to define the scope. The calendar must:

  • Render a month grid for any given month
  • Keep the grid full by showing adjacent-month dates
  • Highlight the current date
  • Display the selected month name
  • Support navigation between months
  • Let users jump back to the current month instantly

For date logic, we'll rely on Day.js, a minimal library for date manipulation. The project setup uses Parcel for bundling, but the code itself is framework-agnostic.

Laying the Foundation

We'll start with markup and styles. The calendar structure consists of three layers: a header with the month label and navigation controls, a day-of-week header, and the date grid. Avoid tables; use lists and CSS Grid for layout instead.

Create a file called index.js inside a src folder. This is where all the markup and logic will live. An index.html at the project root will serve as the host page, linking to the JavaScript file.

<!-- index.js -->
document.getElementById("app").innerHTML = `
<!-- Parent container for the calendar month -->
<div class="calendar-month">
  <!-- The calendar header -->
  <section class="calendar-month-header">
    <!-- Month name -->
    <div
      id="selected-month"
      class="calendar-month-header-selected-month"
    >
      July 2020
    </div>


    <!-- Pagination -->
    <div class="calendar-month-header-selectors">
      <span id="previous-month-selector"><</span>
      <span id="present-month-selector">Today</span>
      <span id="next-month-selector">></span>
    </div>
  </section>
  
  <!-- Calendar grid header -->
  <ol
    id="days-of-week"
    class="day-of-week"
  >
    <li>Mon</li>
    ...
    <li>Sun</li>
  </ol>


  <!-- Calendar grid -->
  <ol
    id="calendar-days"
    class="date-grid"
  >
    <li class="calendar-day">
      <span>
        1
      </span>
      ...
      <span>
        29
      </span>
    </li>
  </ol>
</div>
`;

The HTML boilerplate is minimal—just a container element that the script targets.

<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="app"></div>

    <script src="src/index.js"></script>
  </body>
</html>

Styling the Grid

Add a styles.css file in the same src directory to set up the visual structure using flexbox and CSS Grid.

body {
  --grey-100: #e4e9f0;
  --grey-200: #cfd7e3;
  --grey-300: #b5c0cd;
  --grey-800: #3e4e63;
  --grid-gap: 1px;
  --day-label-size: 20px;
}

.calendar-month {
  position: relative;
  /* Color of the day cell borders */
  background-color: var(--grey-200);
  border: solid 1px var(--grey-200);
}


/* Month indicator and selectors positioning */
.calendar-month-header {
  display: flex;
  justify-content: space-between;
  background-color: #fff;
  padding: 10px;
}


/* Month indicator */
.calendar-month-header-selected-month {
  font-size: 24px;
  font-weight: 600;
}


/* Month selectors positioning */
.calendar-month-header-selectors {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 80px;
}


.calendar-month-header-selectors > * {
  cursor: pointer;
}


/* | Mon | Tue | Wed | Thu | Fri | Sat | Sun | */
.day-of-week {
  color: var(--grey-800);
  font-size: 18px;
  background-color: #fff;
  padding-bottom: 5px;
  padding-top: 10px;
}


.day-of-week,
.days-grid {
  /* 7 equal columns for weekdays and days cells */
  display: grid;
  grid-template-columns: repeat(7, 1fr);
}


.day-of-week > * {
  /* Position the weekday label within the cell */
  text-align: right;
  padding-right: 5px;
}


.days-grid {
  height: 100%;
  position: relative;
  /* Show border between the days */
  grid-column-gap: var(--grid-gap);
  grid-row-gap: var(--grid-gap);
  border-top: solid 1px var(--grey-200);
}


.calendar-day {
  position: relative;
  min-height: 100px;
  font-size: 16px;
  background-color: #fff;
  color: var(--grey-800);
  padding: 5px;
}


/* Position the day label within the day cell */
.calendar-day > span {
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  right: 2px;
  width: var(--day-label-size);
  height: var(--day-label-size);
}

The crucial CSS rule creates seven equal columns for both the day names and the date grid via repeat(). Each date cell gets a consistent min-height of 100px to keep rows uniform.

.day-of-week,
.days-grid {
  /* 7 equal columns for weekdays and days cells */
  display: grid;
  grid-template-columns: repeat(7, 1fr);
}

Import the stylesheet at the top of index.js to connect everything.

import "./styles.css";

At this point, the template is static, with hardcoded dates. That's where Day.js comes in to provide the real calendar data.

Managing Calendar Data with Day.js

Day.js gives us everything needed to work with true dates. We'll use its WeekDay plugin to set Monday as the first day of the week and the weekOfYear plugin for week calculations. After installing, import the library and plugin extensions in index.js.

import dayjs from "dayjs";

Now we can set up the core logic by stripping out the placeholder markup from our template and configuring the page structure:

import dayjs from "dayjs";
import "./styles.css";
const weekday = require("dayjs/plugin/weekday");
const weekOfYear = require("dayjs/plugin/weekOfYear");


dayjs.extend(weekday);
dayjs.extend(weekOfYear);


document.getElementById("app").innerHTML = `
<div class="calendar-month">
  <section class="calendar-month-header">
    <div
      id="selected-month"
      class="calendar-month-header-selected-month"
    >
    </div>
    <div class="calendar-month-header-selectors">
      <span id="previous-month-selector"><</span>
      <span id="present-month-selector">Today</span>
      <span id="next-month-selector">></span>
    </div>
  </section>
  
  <ul
    id="days-of-week"
    class="day-of-week"
  >
  </ul>
  <ul
    id="calendar-days"
    class="days-grid"
  >
  </ul>
</div>
`;

Next, define constants. First, an array of weekday names for the header:

const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];

Then capture the current year in YYYY format:

const INITIAL_YEAR = dayjs().format("YYYY");

And store the current month (numeric, where January is 1) as the initial state:

const INITIAL_MONTH = dayjs().format("M");

Using this data, the calendar grid header can be populated with weekday names by selecting the #days-of-week element and iterating over the WEEKDAYS array.

// Select the calendar grid header element
const daysOfWeekElement = document.getElementById("days-of-week");


// Loop through the array of weekdays
WEEKDAYS.forEach(weekday => {
  // For each item in the array, make a list item element
  const weekDayElement = document.createElement("li");
  // Append a child element inside the list item...
  daysOfWeekElement.appendChild(weekDayElement);
  /// ...that contains the value in the array
  weekDayElement.innerText = weekday;
});

Populating the Calendar Grid

Generating the grid accurately is the core challenge, especially aligning dates with the correct weekday columns. The goal is to have a fully-filled 7-column grid. For example, if the month starts on a Wednesday, the grid must show Monday and Tuesday dates from the previous month to maintain alignment.

The process requires three separate date calculations:

  1. Dates from the current month: Get the total number of days in the selected month using the Day.js daysInMonth method. We create an array and map() it into day objects with properties for the date, the numeric label (dayOfMonth), and a flag (isCurrentMonth) to distinguish dates from other months for styling.
  2. Dates from the previous month: Identify the weekday of the first day in the selected month using the WeekDay plugin. This value tells us how many days from the previous month to display so the first week is complete.
  3. Dates from the next month: Calculate the remaining days needed to fill the last week of the grid, ensuring the calendar always ends on a Saturday.

These calculations are wrapped in helper functions that produce the day objects we need:

function getNumberOfDaysInMonth(year, month) {
  return dayjs(`${year}-${month}-01`).daysInMonth()
}
function createDaysForPreviousMonth(year, month) {
  const firstDayOfTheMonthWeekday = getWeekday(currentMonthDays[0].date);


  const previousMonth = dayjs(`${year}-${month}-01`).subtract(1, "month");
  
  // Account for first day of the month on a Sunday (firstDayOfTheMonthWeekday === 0)
  const visibleNumberOfDaysFromPreviousMonth = firstDayOfTheMonthWeekday ? firstDayOfTheMonthWeekday - 1 : 6


  const previousMonthLastMondayDayOfMonth = dayjs(
    currentMonthDays[0].date
  ).subtract(visibleNumberOfDaysFromPreviousMonth, "day").date();

  return [...Array(visibleNumberOfDaysFromPreviousMonth)].map((day, index) => {    
    return {
      date: dayjs(
        `${previousMonth.year()}-${previousMonth.month() + 1}-${previousMonthLastMondayDayOfMonth + index}`
      ).format("YYYY-MM-DD"),
      dayOfMonth: previousMonthLastMondayDayOfMonth + index,
      isCurrentMonth: false
    };
  });
}
function createDaysForNextMonth(year, month) {
  const lastDayOfTheMonthWeekday = getWeekday(`${year}-${month}-${currentMonthDays.length}`)


  const visibleNumberOfDaysFromNextMonth = lastDayOfTheMonthWeekday ? 7 - lastDayOfTheMonthWeekday : lastDayOfTheMonthWeekday


  return [...Array(visibleNumberOfDaysFromNextMonth)].map((day, index) => {
    return {
      date: dayjs(`${year}-${Number(month) + 1}-${index + 1}`).format("YYYY-MM-DD"),
      dayOfMonth: index + 1,
      isCurrentMonth: false
    }
  })
}

After constructing all three sets of day objects, they are merged into a single array representing the full month view.

let currentMonthDays = createDaysForCurrentMonth(INITIAL_YEAR, INITIAL_MONTH)
let previousMonthDays = createDaysForPreviousMonth(INITIAL_YEAR, INITIAL_MONTH, currentMonthDays[0])
let nextMonthDays = createDaysForNextMonth(INITIAL_YEAR, INITIAL_MONTH)


let days = [...this.previousMonthDays, ...this.currentMonthDays, ...this.nextMonthDays]

Rendering the Dates

Once the data model is built, displaying it is straightforward. Grab the #calendar-days container and create a function to append a day element to it for each item in the array.

function appendDay(day, calendarDaysElement) {
  const dayElement = document.createElement("li");
  const dayElementClassList = dayElement.classList;


  // Generic calendar day class
  dayElementClassList.add("calendar-day");


  // Container for day of month number
  const dayOfMonthElement = document.createElement("span");


  // Content
  dayOfMonthElement.innerText = day.dayOfMonth;


  // Add an extra class to differentiate current month days from prev/next month days
  if (!day.isCurrentMonth) {
    dayElementClassList.add("calendar-day--not-current");
  }


  // Append the element to the container element
  dayElement.appendChild(dayOfMonthElement);
  calendarDaysElement.appendChild(dayElement);
}

Inside the render function, check the isCurrentMonth property. If a day is from the previous or next month, apply a differing CSS style to visually de-emphasize it.

.calendar-day--not-current {
  background-color: var(--grey-100);
  color: var(--grey-300);
}

Adding Interactive Navigation

The calendar is now functional for the current month, but navigation is needed to make it a complete component. We'll create reactive variables for the current, previous, and next month views to simplify the logic:

let currentMonthDays;
let previousMonthDays;
let nextMonthDays;

Then we build the pagination logic in stages. First, create a createCalendar function that takes a year and month, calculates the corresponding date data, and updates the calendar. This involves updating the month label in the header:

function createCalendar(year = INITIAL_YEAR, month = INITIAL_MONTH) {
  document.getElementById("selected-month").innerText = dayjs(
    new Date(year, month - 1)
  ).format("MMMM YYYY");


  // ...

Next, clear out any existing date elements from the grid container:

// ...


  const calendarDaysElement = document.getElementById("calendar-days");
  removeAllDayElements(calendarDaysElement);


  // ...

Recalculate the day objects using the methods we've defined:

//...


currentMonthDays = createDaysForCurrentMonth(
  year,
  month,
  dayjs(`${year}-${month}-01`).daysInMonth()
);


previousMonthDays = createDaysForPreviousMonth(year, month);


nextMonthDays = createDaysForNextMonth(year, month);


const days = [...previousMonthDays, ...currentMonthDays, ...nextMonthDays];


// ...

Finally, re-render the new set of days:

// ...
days.forEach(day => {
  appendDay(day, calendarDaysElement);
});

The method that removes existing child elements loops until the grid is empty.

function removeAllDayElements(calendarDaysElement) {
  let first = calendarDaysElement.firstElementChild;


  while (first) {
    first.remove();
    first = calendarDaysElement.firstElementChild;
  }
}

To activate this, we add a new variable to track the current month selection (initialized to today's date) and then assign event listeners to the navigation buttons. These listeners update the selected month and call createCalendar with the appropriate year and month values.

let selectedMonth = dayjs(new Date(INITIAL_YEAR, INITIAL_MONTH - 1, 1));

Highlighting Today

As a finishing touch, we can visually distinguish the current date. First, store today's date in a variable:

const TODAY = dayjs().format("YYYY-MM-DD");

Then extend the appendDay function with a condition that adds a special class when a rendered day matches today's date.

function appendDay(day, calendarDaysElement) {
  // ...
  if (day.date === TODAY) {
    dayElementClassList.add("calendar-day--today");
  }
}

With the class applied, add a style that draws attention to today.

.calendar-day--today {
  padding-top: 4px;
}


.calendar-day--today > div {
  color: #fff;
  border-radius: 9999px;
  background-color: var(--grey-800);
}

That completes the calendar. The final implementation uses real data from Day.js, is fully navigable, and respects the DOM by updating only the necessary nodes. The entire result—markup, styles, date generation, and event handling—fits within a modest amount of vanilla JavaScript. The final demo shows all the pieces working together.