Danny 💔 1 week ago
i will forever be sad that i didnt get concert tickets for Ariana Grande.
nick 🪄 2 days ago
you can now add extra context to your wishes, for forgotten details or follow-up questions! <3

Dynamic Event Calendar

Written by nick • 09.06.2025

May 26, 2026: I recently updated this tutorial to add clickable event links (a wish from Sybilla on the wishlist) and support for HTML font icons. If you are already using the older version on your site, the step-by-step update guide will show you how to easily add both new features manually without losing your current setup. <3

In this tutorial, you'll learn how to build a dynamic event calendar using HTML, CSS, and JavaScript.

Instead of manually editing the HTML of the calendar every month, you'll simply add all your events into the JavaScript section it'll automatically stay up to date!



1. The HTML

If you are adding this calendar to an existing website, you can easily skip this step. However, to keep things clean and easy to follow for this tutorial, it's recommended starting from scratch by creating a fresh index.html file on your desktop using this template:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dynamic Event Calendar</title>
    <style>
    /* We'll add the CSS here later */
    </style>
  </head>
  <body>
    <!-- Our calendar HTML will go here -->
  </body>
</html>

2. Adding the calendar HTML structure

We need to set up the HTML elements that will hold our calendar. Because JavaScript will generate all the actual dates and months dynamically later, these elements act as placeholders for the layout.

You can place this snippet anywhere you want the calendar to show up on your page, like a sidebar, a footer, or right in your main content area.

For this tutorial, we will just drop it inside the main <body> tags of your index.html file:

<div id="calendar-container">
  <div id="calendar-nav"></div>
  <div id="calendar"></div>
</div>

Essentially, this adds a main wrapper to keep the whole calendar together, a navigation container to switch between months, and an empty grid where the actual days will be drawn.


3. Styling our calendar with CSS

To keep things simple, we will put all our CSS inside a <style> tag in the <head> section. Feel free to move it to a separate calendar.css file or your main style.css later. I also left comments in the CSS to help you find and tweak the style easily! <3

/* Calendar Container */
#calendar-container {
    background: #fff;
    border: 1px solid rgba(0,0,0, 0.1);
    width: max-content;
    max-width: 100%;
}

/* Calendar Navigation */
#calendar-nav {
    background: #0e4a5f;
    color: #fff;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 1px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 5px 10px;
}

#calendar-nav button {
    background: transparent;
    border: 0;
    color: inherit;
    font-size: 8pt;
    cursor: pointer;
}

#calendar-title {
    text-align: center;
    display: block;
    width: 100%;
}

/* Calendar Grid */
#calendar {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
}

/* Weekday Header */
.weekday {
    background: #f0f0f0;
    font-weight: bold;
    padding: 5px;
    text-transform: uppercase;
    border-bottom: 1px solid #ccc;
    text-align: center;
}

/* Day Cells */
.day {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 30px;
    position: relative;
    background: rgba(255, 255, 255, 0.05);
    box-sizing: border-box;
    padding: 0 5px;
    text-align: center;
}

.alt-row {
    background: rgba(0, 0, 0, 0.05);
}

.current-day {
    font-weight: bold;
    background: #fff;
    border: 1px solid #ccc;
    box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
}

.adjacent-month {
    opacity: 0.4;
}

.number-container {
    display: flex;
    align-items: center;
    gap: 4px;
}

/* Event Icons and Tooltips */
.event-icon {
    cursor: help;
    display: inline-block;
    position: relative;
    text-decoration: none;
    color: inherit;
}

.tooltip {
    display: none;
    position: absolute;
    left: 20px;
    background: #333;
    color: #fff;
    padding: 5px 10px;
    white-space: nowrap;
    border-radius: 3px;
    z-index: 10;
}

.event-icon:hover .tooltip {
    display: block;
}

4. Setting up the data

Add a <script> block to your index.html, right before the closing </body> tag. Every piece of JavaScript we cover next will go right inside this block!

<script>
  // All the JavaScript blocks below will be pasted right here
</script>

Before writing the logic, we need to provide the calendar with its basic settings and the events you want to display. Paste this block inside your script tags to set up your configuration and sample data:

// Calendar configuration
const calendarConfig = {
  showNav: true, // Show month navigation? true or false
  prevLabel: "❮", // Symbol for the Prev month link
  nextLabel: "❯", // Symbol for the Next month link
  locale: "en", // Change to "de", "fr", etc. if needed
  weekStartsOn: 0, // 0 = Sunday, 1 = Monday
};

// Events for specific dates (one-time)
const events = {
  "2025-04-21": { icon: "✝️", text: "Easter Monday", url: "https://example.com/easter" }, // optional: Events as linked icons
};

// Recurring events every year (e.g. birthdays)
const recurringEvents = {
  "01-01": { icon: "🎉", text: "New Year's Day" },
  "05-13": { icon: "🎂", text: "Nick's Birthday" },
  "10-31": { icon: "<i class='fa-solid fa-cake'></i>", text: "Danny's Birthday & Halloween" }, // optional: HTML instead of Emojis, e.g. using FontAwesome
  "12-24": { icon: "🎄", text: "Christmas" },
  "06-01": { icon: "🏳️‍🌈", text: "Pride Month" },
};

Each event needs an icon (like an emoji) and a text description for the tooltip on hover. You can also add an optional url property, which automatically turns the event icon into a clickable link that opens in a new tab.


5. Adding the calendar logic

Finally, we need to add the code that actually makes our calendar work. This script connects to your HTML placeholders, calculates the days for each month, loops through your configuration data to render your events, and updates the view when you switch months.

Paste this final block inside your script tags, right underneath your configuration and events data:

// Weekday headers
function getWeekdays(startDay = 0) {
  const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
  return days.slice(startDay).concat(days.slice(0, startDay));
}

const calendarGrid = document.getElementById("calendar");
const navigationBar = document.getElementById("calendar-nav");

let currentDate = new Date();

// Utility to create HTML elements for the calendar
const createElement = (tag, text = "", className = "") => {
  const element = document.createElement(tag);

  if (text) element.textContent = text;
  if (className) element.className = className;
  return element;
};

// Main function to build the calendar
function buildCalendar(date) {
  calendarGrid.innerHTML = "";
  navigationBar.innerHTML = "";

  const year = date.getFullYear();
  const month = date.getMonth();
  const today = new Date();

  // Get localized month name
  const monthName = date.toLocaleString(calendarConfig.locale || "en", {
    month: "long",
  });

  // Create title and navigation buttons
  const titleElement = createElement("div", `${monthName} ${year}`, "");
  titleElement.id = "calendar-title";

  if (calendarConfig.showNav) {
    const prevButton = createElement("button", calendarConfig.prevLabel);
    prevButton.onclick = () => changeMonth(-1);

    const nextButton = createElement("button", calendarConfig.nextLabel);
    nextButton.onclick = () => changeMonth(1);

    navigationBar.append(prevButton, titleElement, nextButton);
  } else {
    navigationBar.appendChild(titleElement);
  }

  // Add weekday headers
  getWeekdays(calendarConfig.weekStartsOn).forEach((day) =>
    calendarGrid.appendChild(createElement("div", day, "weekday"))
  );

  let firstDay = new Date(year, month, 1).getDay();
  firstDay = (firstDay - calendarConfig.weekStartsOn + 7) % 7;
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const daysInPrevMonth = new Date(year, month, 0).getDate();

  const totalCells = firstDay + daysInMonth;
  const totalWeeks = Math.ceil(totalCells / 7);
  const neededCells = totalWeeks * 7;

  // Fill in previous month days (greyed out)
  for (let i = firstDay - 1; i >= 0; i--) {
    const day = daysInPrevMonth - i;
    const dayCell = createElement("div", "", "day adjacent-month");
    const container = createElement("div", "", "number-container");

    container.textContent = day;
    dayCell.appendChild(container);
    calendarGrid.appendChild(dayCell);
  }

  // Fill in current month days
  for (let day = 1; day <= daysInMonth; day++) {
    const dayCell = createElement("div", "", "day");

    // Alternate row styling
    if (Math.floor((firstDay + day - 1) / 7) % 2 === 1) {
      dayCell.classList.add("alt-row");
    }

    const dateString = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
    const recurringKey = `${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
    const container = createElement("div", "", "number-container");

    // Check for event (specific or recurring)
    const event = events[dateString] || recurringEvents[recurringKey];

    if (event) {
      const iconTag = event.url ? "a" : "span";
      const iconSpan = createElement(iconTag, "", "event-icon");
      iconSpan.innerHTML = event.icon;

      if (event.url) {
        iconSpan.href = event.url;
        iconSpan.target = "_blank";
      }

      const tooltip = createElement("span", event.text, "tooltip");

      iconSpan.appendChild(tooltip);
      container.appendChild(iconSpan);
    } else {
      container.textContent = day;
    }

    dayCell.appendChild(container);

    // Highlight current day
    if (
      day === today.getDate() &&
      month === today.getMonth() &&
      year === today.getFullYear()
    ) {
      dayCell.classList.add("current-day");
    }

    calendarGrid.appendChild(dayCell);
  }

  // Fill in next month days to complete row
  const filledCells = calendarGrid.querySelectorAll(".day").length;
  const nextMonthDays = neededCells - filledCells;

  for (let day = 1; day <= nextMonthDays; day++) {
    const dayCell = createElement("div", "", "day adjacent-month");
    const container = createElement("div", "", "number-container");

    container.textContent = day;
    dayCell.appendChild(container);
    calendarGrid.appendChild(dayCell);
  }
}

// Function to change calendar month
function changeMonth(offset) {
  currentDate.setMonth(currentDate.getMonth() + offset);
  buildCalendar(currentDate);
}

// Show the calendar on page load
buildCalendar(currentDate);

6. Advanced: Customizing with HTML icon fonts

If you want to use an icon library like Font Awesome or Phosphor Icons instead of standard emojis, you can drop the icon HTML snippet directly into the icon field. For example:

"05-13": { icon: "<i class='fa-solid fa-cake-candles'></i>", text: "Nick's Birthday" }

Because your icon HTML is already wrapped in double quotes (" "), always use single quotes (') for the classes inside it. Mixing double quotes inside double quotes will break your JavaScript.

This works: icon: "<i class='fa-solid fa-cake-candles'></i>"
This breaks: icon: "<i class="fa-solid fa-cake-candles"></i>"

To make icon libraries work, do not forget to link their stylesheet inside your website's <head> section first:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" />

7. The full code

Here's the complete index.html file with all the HTML, CSS, and JavaScript combined! <3

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Event Calendar</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" />
    <style>
      /* Calendar Container */
      #calendar-container {
        background: #fff;
        border: 1px solid rgba(0,0,0, 0.1);
        width: max-content;
        max-width: 100%;
      }

      /* Calendar Navigation */
      #calendar-nav {
        background: #0e4a5f;
        color: #fff;
        font-weight: bold;
        text-transform: uppercase;
        letter-spacing: 1px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 5px 10px;
      }

      #calendar-nav button {
        background: transparent;
        border: 0;
        color: inherit;
        font-size: 8pt;
        cursor: pointer;
      }

      #calendar-title {
        text-align: center;
        display: block;
        width: 100%;
      }

      /* Calendar Grid */
      #calendar {
        display: grid;
        grid-template-columns: repeat(7, 1fr);
      }

      /* Weekday Header */
      .weekday {
        background: #f0f0f0;
        font-weight: bold;
        padding: 5px;
        text-transform: uppercase;
        border-bottom: 1px solid #ccc;
        text-align: center;
      }

      /* Day Cells */
      .day {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 30px;
        position: relative;
        background: rgba(255, 255, 255, 0.05);
        box-sizing: border-box;
        padding: 0 5px;
        text-align: center;
      }

      .alt-row {
        background: rgba(0, 0, 0, 0.05);
      }

      .current-day {
        font-weight: bold;
        background: #fff;
        border: 1px solid #ccc;
        box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
      }

      .adjacent-month {
        opacity: 0.4;
      }

      .number-container {
        display: flex;
        align-items: center;
        gap: 4px;
      }

      /* Event Icons and Tooltips */
      .event-icon {
        cursor: help;
        display: inline-block;
        position: relative;
        text-decoration: none;
      }

      .tooltip {
        display: none;
        position: absolute;
        left: 20px;
        background: #333;
        color: #fff;
        padding: 5px 10px;
        white-space: nowrap;
        border-radius: 3px;
        z-index: 10;
      }

      .event-icon:hover .tooltip {
        display: block;
      }
    </style>
  </head>
  <body>
    <div id="calendar-container">
      <div id="calendar-nav"></div>
      <div id="calendar"></div>
    </div>

    <script>
      // Calendar configuration
      const calendarConfig = {
        showNav: true, // Show month navigation? true or false
        prevLabel: "❮", // Symbol for the Prev month link
        nextLabel: "❯", // Symbol for the Next month link
        locale: "en", // Change to "de", "fr", etc. if needed
        weekStartsOn: 0, // 0 = Sunday, 1 = Monday
      };

      // Events for specific dates (one-time)
      const events = {
        "2025-04-21": { icon: "✝️", text: "Easter Monday", url: "https://example.com/easter" }, // optional: Events as linked icons
      };

      // Recurring events every year (e.g. birthdays)
      const recurringEvents = {
        "01-01": { icon: "🎉", text: "New Year's Day" },
        "05-13": { icon: "🎂", text: "Nick's Birthday" }, 
        "10-31": { icon: "<i class='fa-solid fa-cake'></i>", text: "Danny's Birthday & Halloween" }, // optional: HTML instead of Emojis using FontAwesome
        "12-24": { icon: "🎄", text: "Christmas" },
        "06-01": { icon: "🏳️‍🌈", text: "Pride Month" },
      };

      // Weekday headers
      function getWeekdays(startDay = 0) {
        const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
        return days.slice(startDay).concat(days.slice(0, startDay));
      }

      const calendarGrid = document.getElementById("calendar");
      const navigationBar = document.getElementById("calendar-nav");

      let currentDate = new Date();

      // Utility to create HTML elements for the calendar
      const createElement = (tag, text = "", className = "") => {
        const element = document.createElement(tag);

        if (text) element.textContent = text;
        if (className) element.className = className;
        return element;
      };

      // Main function to build the calendar
      function buildCalendar(date) {
        calendarGrid.innerHTML = "";
        navigationBar.innerHTML = "";

        const year = date.getFullYear();
        const month = date.getMonth();
        const today = new Date();

        // Get localized month name
        const monthName = date.toLocaleString(calendarConfig.locale || "en", {
          month: "long",
        });

        // Create title and navigation buttons
        const titleElement = createElement("div", `${monthName} ${year}`, "");
        titleElement.id = "calendar-title";

        if (calendarConfig.showNav) {
          const prevButton = createElement("button", calendarConfig.prevLabel);
          prevButton.onclick = () => changeMonth(-1);

          const nextButton = createElement("button", calendarConfig.nextLabel);
          nextButton.onclick = () => changeMonth(1);

          navigationBar.append(prevButton, titleElement, nextButton);
        } else {
          navigationBar.appendChild(titleElement);
        }

        // Add weekday headers
        getWeekdays(calendarConfig.weekStartsOn).forEach((day) =>
          calendarGrid.appendChild(createElement("div", day, "weekday"))
        );

        let firstDay = new Date(year, month, 1).getDay();
        firstDay = (firstDay - calendarConfig.weekStartsOn + 7) % 7;
        const daysInMonth = new Date(year, month + 1, 0).getDate();
        const daysInPrevMonth = new Date(year, month, 0).getDate();

        const totalCells = firstDay + daysInMonth;
        const totalWeeks = Math.ceil(totalCells / 7);
        const neededCells = totalWeeks * 7;

        // Fill in previous month days (greyed out)
        for (let i = firstDay - 1; i >= 0; i--) {
          const day = daysInPrevMonth - i;
          const dayCell = createElement("div", "", "day adjacent-month");
          const container = createElement("div", "", "number-container");

          container.textContent = day;
          dayCell.appendChild(container);
          calendarGrid.appendChild(dayCell);
        }

        // Fill in current month days
        for (let day = 1; day <= daysInMonth; day++) {
          const dayCell = createElement("div", "", "day");

          // Alternate row styling
          if (Math.floor((firstDay + day - 1) / 7) % 2 === 1) {
            dayCell.classList.add("alt-row");
          }

          const dateString = `${year}-${String(month + 1).padStart(2,"0")}-${String(day).padStart(2, "0")}`;
          const recurringKey = `${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
          const container = createElement("div", "", "number-container");

          // Check for event (specific or recurring)
          const event = events[dateString] || recurringEvents[recurringKey];

          if (event) {
            const iconTag = event.url ? "a" : "span";
            const iconSpan = createElement(iconTag, "", "event-icon");
            iconSpan.innerHTML = event.icon;
            
            if (event.url) {
              iconSpan.href = event.url;
              iconSpan.target = "_blank";
            }

            const tooltip = createElement("span", event.text, "tooltip");

            iconSpan.appendChild(tooltip);
            container.appendChild(iconSpan);
          } else {
            container.textContent = day;
          }

          dayCell.appendChild(container);

          // Highlight current day
          if (
            day === today.getDate() &&
            month === today.getMonth() &&
            year === today.getFullYear()
          ) {
            dayCell.classList.add("current-day");
          }

          calendarGrid.appendChild(dayCell);
        }

        // Fill in next month days to complete row
        const filledCells = calendarGrid.querySelectorAll(".day").length;
        const nextMonthDays = neededCells - filledCells;

        for (let day = 1; day <= nextMonthDays; day++) {
          const dayCell = createElement("div", "", "day adjacent-month");
          const container = createElement("div", "", "number-container");

          container.textContent = day;
          dayCell.appendChild(container);
          calendarGrid.appendChild(dayCell);
        }
      }

      // Function to change calendar month
      function changeMonth(offset) {
        currentDate.setMonth(currentDate.getMonth() + offset);
        buildCalendar(currentDate);
      }

      // Show the calendar on page load
      buildCalendar(currentDate);
    </script>
  </body>
</html>

Changelog

26.05.2026
- event icons can be linked to a website now
- the event icon supports html now
- smaller text changes here and there