Building a full page calendar with React

As a developer, I can’t help but get excited when I see immersive apps built on the web. I love it when I get to build things that are a little bit “out of the ordinary”.

I recently built a full page calendar widget. At first I thought I’d reach for a library, but I found myself a little disappointed with the options in the ecosystem. More importantly, I realized that it’s actually not very hard to build a full page calendar using modern web technologies.

Because it doesn’t take much code and also because there’s so many custom use cases for a calendar, I think having a solid starting point that you can copy into your project and adapt as necessary will prove more useful than importing a third party library and trying to extend it. So in this article, we’re going to build the calendar experience shown below. If you’re just interested in the code, feel free to open the code sandbox and view the entire project 🙂

Beyond using React, I’m going to try to keep the technology and patterns as non-controversial and “standard” as I can, in the hopes that this code will be useful to as many of you as possible! For example, I’ll be using global CSS to style the calendar because I know there’s lots of different ways to encapsulate styles. I’ll let you adapt the style code as necessary in your own projects from the sandbox link. Some key technologies we’ll be using are:

Note: This is not a step-by-step article. I’ll be detailing the less-obvious/ more-interesting patterns and approaches in this article, but you should open the sandbox and look at the code alongside this article to see how everything fits together!


Component Structure and Props API

The <Calendar /> component itself will handle :

  • Displaying a month grid for the given month/year
  • Displaying headers for the days of the week
  • Displaying the current year/ month
  • Providing controls for changing year/month

We’ll build the calendar as a controlled component with the props below. Also, see <App.js /> in the code sandbox.

<Calendar  
  // It's up to you to render the contents of each calendar day.  
  // Pass a function to renderCalendarDay which returns a  
  // node given an object with the following properties:  
  //  {  
  //    dateString,       e.g. 2021-08-30  
  //    dayOfMonth,       e.g. 30  
  //    isCurrentMonth,   true/false  
  //    isNextMonth,      true/false  
  //    isPreviousMonth,  true/false, 
  //  }  
  renderCalendarDay={(calendarDayObject) => ...}  

  // e.g. [2021, 08] for August 2021  
  yearAndMonth={[yearInt, monthInt]}  
  
  // onChange handler (you can just pass the setter  
  // from setState if you'd like)   
  onYearAndMonthChange={(yearAndMonth) => ...}
/>

Note: The calendar component does not render the day headers within each grid item. It’s up to you to do so. However, the code sandbox does export a simple component named <CalendarDayHeader /> to help with this. We’ll talk more about why we’ve opted to do this in the “Next Steps For You” section at the end of this article.

Rendering The Grid with JS

We can use CSS Grid to do the heavy lifting of the layout for us, but we need to use javascript to figure out how many grid items to render. We also need to render some grid items for the previous/ next months as necessary to fill out the calendar.

In calendar.js, we’ll build out an array with an object for each day that needs to be rendered. The objects can encode the information necessary to render the day information.

Note: this strategy is adapted from another excellent article over on CSS Tricks! https://css-tricks.com/how-to-make-a-monthly-calendar-with-real-data

// calendar.js
const [year, month] = yearAndMonth;

let currentMonthDays = createDaysForCurrentMonth(year, month);

let previousMonthDays = createDaysForPreviousMonth(  
  year, 
  month, 
  currentMonthDays
);

let nextMonthDays = createDaysForNextMonth(
  year,   
  month,   
  currentMonthDays
);

let calendarGridDayObjects = [ 
  ...previousMonthDays, 
  ...currentMonthDays,
  ... nextMonthDays
];

A day object will look like:

{  
  dateString,       // e.g. 2021-08-30  
  dayOfMonth,       // e.g. 30  
  isCurrentMonth,   // true/false  
  isNextMonth,      // true/false  
  isPreviousMonth,  // true/false
}

Let’s take a look at createDaysForCurrentMonth. It’s actually pretty straightforward with Day.js!

export function getNumberOfDaysInMonth(year, month) {
  return dayjs(`${year}-${month}-01`).daysInMonth();
}

export function createDaysForCurrentMonth(year, month) {
  return [ 
    ...Array(getNumberOfDaysInMonth(year, month))  
  ].map((_, index) => {    
    return { 
      dateString: dayjs(`${year}-${month}-${index + 1}`)
        .format("YYYY-MM-DD"),
      dayOfMonth: index + 1,
      isCurrentMonth: true,
      isNextMonth: false,
      isPreviousMonth: false
     };
   });
}

See helpers.js for the implementation of all helper functions!

Now that we’ve got an array of day objects, rendering the grid is pretty simple in the component file. We don’t even need to create a nested DOM structure for the rows! CSS Grid will take care of this for us!

<div className="calendar-root">  
  <div className="days-of-week">    
    {daysOfWeek.map((day, index) => (      
      <div        
        key={day}        
        className="day-of-week-header-cell"    
      >        
        {day}      
      </div>    
    ))}  
  </div>  
  <div className="days-grid">    
    {calendarGridDayObjects.map((day) => (      
      <div        
        key={day.dateString}        
        className={classNames(
          "day-grid-item-container", 
          { "current-month": day.isCurrentMonth }
        )}      
      >        
        <div className="day-content-wrapper">{renderDay(day)}</div>     
      </div>    
    ))}  
  </div>
</div>

Styling The Grid With CSS

Alright, let’s get styling! We can start with a root class where we’ll define some helpful variables to keep our styles clean.

.calendar-root { 
  --grid-gap: 2px; 
  --grid-height: 700px; 
  --grid-background-color: rgb(211, 205, 198);  
  --grid-foreground-color: white;
}

Now, let’s define the styles for the grid!

.days-grid {  
  position: relative; 
  width: 100%;  
  height: var(--grid-height);  
  box-sizing: border-box;  
  display: grid;  
  grid-template-columns: repeat(7, 1fr);  
  grid-column-gap: var(--grid-gap);  
  grid-row-gap: var(--grid-gap);  
  border: var(--grid-gap) solid var(--grid-background-color);   
  background-color: var(--grid-background-color);
}

.day-grid-item-container {  
  background-color: var(--grid-foreground-color);
}

Thats it! 🎉 There’s more css to look at in the code sandbox, but to style the bulk of the calendar, all we need is the code above. Some notes:

  • grid-template-columns: repeat(7, 1fr) tells the browser to layout 7 columns and distribute their widths equally.
  • We’ve given the grid a background color which is used as the item borders and explicitly defined the item foreground color. This lets us make the grid column and row gaps bigger/ smaller as we please.
  • The grid has a fixed height, so when a month needs only 5 rows, each row will be taller in comparison to when a month needs 6 rows.

Next Steps For You

Play around with the code sandbox and adapt the code as necessary in your project! You have full control over over the day items, so you can do things like render interactive calendar item labels using your application data.

You’ll just want to make sure you render a <CalendarDayHeader /> inside the container element so that the calendar day is displayed. It’s structured as a separate component so that you can more easily attach an onClick handler to the container to catch click events bubbling up from the header. (This covers the use case of wanting to open a “new event” dialog when a user clicks somewhere inside the calendar day.)

If you think improvements can be made to this implementation or if you have any questions, please leave a comment! Happy coding!