Build Custom Interactive Calendar React Without UI Libraries

In this post, you will learn how to build a custom interactive calendar in React without any additional libraries.

finished project

This is how it will look. We have a calendar where we can jump between days, switch months, jump to today, and select a date to check what meetings are planned for that day.

We won't use any UI libraries to create this project, but we will need a library to work with dates in JavaScript.

The default date handling in JavaScript is really painful and not flexible.

luxon website

A library called Luxon is a popular solution for working with dates in a comfortable and efficient manner.

DateTime.now().setZone('America/New_York').minus({weeks:1}).endOf('day').toISO();

We can write our code like this. Even if you've never worked with Luxon, you can understand what we are doing here. First, we get the current date. Then, we set the timezone, subtract one week from this date, and get the end of the day. The code is extremely readable, easy to support, and it will help us a lot in creating an interactive calendar with React.

Initial Project

Here, I have already prepared an empty project for us.

import Calendar from "./Calendar";

const App = () => {
  return <Calendar />;
};

I have an App component that renders a Calendar component and an empty component.

import './calendar.css'
const Calendar = () => {
  return 'calendar';
};

The only addition is the styles I prepared for our calendar. You can find the complete CSS in the source code at the end of this article.

Our first step now is to install Luxon in our project.

npm i luxon

Additionally, I want to install one more library called classnames. It will allow us to concatenate classes in a convenient way.

npm i classnames

Preparing State

The two most important things for our calendar are to prepare an array of weekdays and to generate all the dates of the month that we want to render.

import { Info } from "luxon";
const Calendar = () => {
  const today = DateTime.local();
  const weekDays = Info.weekdays("short");
}

Here we leverage Luxon to create a today DateTime and an array of weekdays in the short format.

today

This is how we can work with Luxon DateTime. As you can see, it is simple and efficient.

Now, we need to determine the first day of the month and store it in the state, as it will change every time we jump between months.

const Calendar = () => {
  ...
  const [firstDayOfActiveMonth, setFirstDayOfActiveMonth] = useState(
      today.startOf("month")
  );
}

We store firstDayOfActiveMonth in the state, and its initial value is based on today, which we use to get the start of the month.

Now, we need to prepare an array of dates for the entire month that we want to render, including the end of the previous month or the beginning of the next month if they fall within the same weeks as the start and end of the current month.

With Luxon, we can accomplish this by using an interval between two dates.

const Calendar = () => {
  ...
  const daysOfMonth = Interval.fromDateTimes(
    firstDayOfActiveMonth.startOf("week"),
    firstDayOfActiveMonth.endOf("month").endOf("week")
  )
    .splitBy({ day: 1 })
    .map((day) => day.start);
}

We generate an interval between the start of the month and the end of the month, including the start and end of the week days. Then, we split the interval by 1 day and map a date for each day.

dates arr

This is how our array looks. Each element is a Luxon DateTime object representing a specific day. As you can see, Luxon allows us to generate such an array in just 3 lines of code.

Rendering markup

Let's begin with some rendering.

return (
  <div className="calendar-container">
    <div className="calendar">
      <div className="calendar-headline">
        <div className="calendar-headline-month">
          {firstDayOfActiveMonth.monthShort}, {firstDayOfActiveMonth.year}
        </div>
        <div className="calendar-headline-controls">
          <div
            className="calendar-headline-control"
          >
            «
          </div>
          <div
            className="calendar-headline-control calendar-headline-controls-today"
          >
            Today
          </div>
          <div
            className="calendar-headline-control"
          >
            »
          </div>
        </div>
      </div>
    </div>
  </div>
);

headline

Here is some markup for the headline of the calendar. We render the month name, year, and controls.

Now, let's render a list of weekdays and a grid for the month.

return (
    <div className="calendar-container">
      <div className="calendar">
        <div className="calendar-headline">
          ...
        </div>
        <div className="calendar-weeks-grid">
          {weekDays.map((weekDay, weekDayIndex) => (
            <div key={weekDayIndex} className="calendar-weeks-grid-cell">
              {weekDay}
            </div>
          ))}
        </div>
        <div className="calendar-grid">
          {daysOfMonth.map((dayOfMonth, dayOfMonthIndex) => (
            <div
              key={dayOfMonthIndex}
              className="calendar-grid-cell"
            >
              {dayOfMonth.day}
            </div>
          ))}
        </div>
      </div>
    </div>
  );

Here we mapped through weekDays and daysOfMonth to render the entire UI of the calendar.

grid

As you can see, the entire calendar is correctly rendered.

Adding Meetings

The next feature we need to implement is rendering a list of meetings on the right side of our calendar. To achieve this, we first need to pass this meeting information to our calendar component.

const App = () => {
  const meetings = {
    "2024-04-05": ["Drink Coffee", "Learn React", "Sleep"],
    "2024-04-06": ["Drink Coffee", "Learn Angular", "Sleep"],
  };
  return <Calendar meetings={meetings} />;
};

const Calendar = ({ meetings }) => {
  ...
}

We store meetings as an object with a specific day as the key. This way, we can retrieve the list of meetings for any date.

Now, we need to store an active day in the state. This is necessary because when we click on a specific day to show its meetings, we need to store and update this information.

const Calendar = ({ meetings }) => {
  const [activeDay, setActiveDay] = useState(null);
  const activeDayMeetings = meetings[activeDay?.toISODate()] ?? [];
  ...
}

We will store the Luxon DateTime object in activeDay, which will allow us to easily retrieve and display a list of meetings for the active day. With this setup, we have all the necessary information to render the list of meetings.

return (
  <div className="calendar-container">
    <div className="calendar">
      ...
    </div>
    <div className="schedule">
      <div className="schedule-headline">
        {activeDay === null && <div>Please select a day</div>}
        {activeDay && (
          <div>{activeDay.toLocaleString(DateTime.DATE_MED)}</div>
        )}
      </div>
      <div>
        {activeDay && activeDayMeetings.length === 0 && (
          <div>No Planned Meetings Today</div>
        )}
        {activeDay && activeDayMeetings.length > 0 && (
          <>
            {activeDayMeetings.map((meeting, meetingIndex) => (
              <div key={meetingIndex}>{meeting}</div>
            ))}
          </>
        )}
      </div>
    </div>
  </div>
);

If there is no activeDay, we render a label saying "Please select a day". Once a day is selected, we render at least a headline. If there are any meetings for this day, we render them as a list; otherwise, we render "No Planned Meetings Today".

The meetings list functionality is complete, but we still need to implement a click event to set an activeDay.

{daysOfMonth.map((dayOfMonth, dayOfMonthIndex) => (
  <div
    ...
    onClick={() => setActiveDay(dayOfMonth)}
  >
    {dayOfMonth.day}
  </div>
))}

We use setActiveDay to update the state to the clicked day.

select day

Now you can see that a day can be selected, and we render the correct information accordingly.

Highlighting dates

One more feature we're missing is highlighting the active day. To accomplish this, we can utilize the classnames library that we've already installed.

import classnames from "classnames";
const Calendar = ({ meetings }) => {
  ...
  {daysOfMonth.map((dayOfMonth, dayOfMonthIndex) => (
    <div
      key={dayOfMonthIndex}
      className={classnames({
        "calendar-grid-cell": true,
        "calendar-grid-cell-inactive":
          dayOfMonth.month !== firstDayOfActiveMonth.month,
        "calendar-grid-cell-active":
          activeDay?.toISODate() === dayOfMonth.toISODate(),
      })}
      onClick={() => setActiveDay(dayOfMonth)}
    >
      {dayOfMonth.day}
    </div>
  ))}
}

Now, instead of applying a single class to our cell, we use the classnames function to concatenate them dynamically. By default, we set calendar-grid-cell for all cells. If the day is not in the current month, we add calendar-grid-cell-inactive. When the day is active, we add calendar-grid-cell-active.

classnames

As you can see, now the active day is correctly highlighted.

The last feature we're missing is arrow navigation and jumping to today. To implement this, we need to create several functions for changing firstDayOfActiveMonth. These functions will handle navigating to previous and next months, as well as jumping to today's date.

const Calendar = ({ meetings }) => {
  ...
  const goToPreviousMonth = () => {
    setFirstDayOfActiveMonth(firstDayOfActiveMonth.minus({ month: 1 }));
  };
  const goToNextMonth = () => {
    setFirstDayOfActiveMonth(firstDayOfActiveMonth.plus({ month: 1 }));
  };
  const goToToday = () => {
    setFirstDayOfActiveMonth(today.startOf("month"));
  };
  ...
}

Here, we use Luxon to subtract or add a month from the firstDayOfActiveMonth. Now, we just need to add click events to trigger these functions.

<div className="calendar-headline-controls">
  <div
    className="calendar-headline-control"
    onClick={() => goToPreviousMonth()}
  >
    «
  </div>
  <div
    className="calendar-headline-control calendar-headline-controls-today"
    onClick={() => goToToday()}
  >
    Today
  </div>
  <div
    className="calendar-headline-control"
    onClick={() => goToNextMonth()}
  >
    »
  </div>
</div>

We can navigate through the calendar by clicking on our headline controls.

finished project

Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!

📚 Source code of what we've done
Don't miss a thing!
Follow me on Youtube, Twitter or Instagram.
Oleksandr Kocherhin
Oleksandr Kocherhin is a full-stack developer with a passion for learning and sharing knowledge on Monsterlessons Academy and on his YouTube channel. With around 15 years of programming experience and nearly 9 years of teaching, he has a deep understanding of both disciplines. He believes in learning by doing, a philosophy that is reflected in every course he teaches. He loves exploring new web and mobile technologies, and his courses are designed to give students an edge in the fast-moving tech industry.