Build a gCal/Outlook like Calendar with React and CSS Grid

Let's say you need to build a calendar in your application that works like the one in popular applications like Outlook, Google Calendar or iCal (oh is it just called Calendar now? neat). Unless you have time to build one from scratch, wouldn't it be nice if the wonderful dev community provided you with a nifty little library instead? As in many other situations, there's someone who invented the wheel for us. I'm gonna show you how to build such calendar with one of these libraries and supercharge it with some CSS wizardry.


TL;DR

Fine, here's the result: https://codesandbox.io/s/zqk8vmqn3m


After narrowing down my choice to a couple of libraries, I settled on react-big-calendar. It's well supported, uses flexbox and it's highly customizable, which is great if you want to add things like drag-and-drop, localization and other goodies.

A quick yarn add react-big-calendar in addition to installing moment-mini (cause we like small packages) and you're good to go. The base setup consists in importing the package and setting a localizer for handling date formatting and culture localization. I'll use moment, so the first lines of my Calendar component would look like this:

/* Calendar.js */

import React from "react";
import BigCalendar from "react-big-calendar";
import moment from "moment-mini";

BigCalendar.setLocalizer(BigCalendar.momentLocalizer(moment));

The library comes with some default styles which provide a visual foundation of the calendar view, so stick this in there:

import "react-big-calendar/lib/css/react-big-calendar.css";

Before rendering the library's main component, you need to have some data source for the calendar to pull events from. The list of events should be an array of JavaScript objects, like the following:

/* events.js */

export default [
  {
    id: 0,
    title: "Smelly Pillow Fight",
    start: new Date(2018, 3, 27, 8, 0, 0, 0),
    end: new Date(2018, 3, 27, 11, 0, 0, 0),
    type: "global",
  },
  {
    id: 1,
    title: "Buggalo Riding",
    start: new Date(2018, 3, 27, 11, 0, 0, 0),
    end: new Date(2018, 3, 27, 12, 0, 0, 0),
    type: "martian",
  },
  ...
];

After having a serious data source, unlike mine, use the calendar component:

/* Calendar.js /*

...

import './events'

...

export default () => (
  <BigCalendar
    defaultDate={new Date(2018, 3, 27)}
    defaultView="day"
    events={events}
    views={["week", "day"]}
  />
);

That will create a calendar with a week and day views, setting the initial date to be 27/03/2018, in line with what defined in the events.js file. You should see something like this:

calendar-view-1-1

Kinda horrible isn't it? What's that messy overlapping? And why everything looks so 1992? Let's spice things up a notch.

Unfortunately the overlapping seems to be a specific layout choice at the moment. Here's the response from one of the creators:

There isn't a way to adjust how RBC does layout at the moment, tho we've talked about making it pluggable. RBC picked a layout strategy that we think is useful and appropriate for a wide variety of cases, like having lots of events in a day, and we're happy with the results generally.

Provided there are other ways to deal with that, there's something all the cool CSS kids started to use these days: CSS Grids. Unless you have to support IE8-9 or Opera Mini (WHY), grids are a now wildly supported fantastic tool to create such layouts. Let's do eeeet.

First, some basic style overrides to make this thing work:

/* index.css */

.rbc-events-container {
  display: grid;
  grid-template-rows: repeat(24, 40px); /* 24 hours; each slot 40px tall */
}

.rbc-day-slot .rbc-event {
  position: static; /* don't use the default's relative positioning */
}

Next step would be to set grid-row to each .rbc-event. Since you're not masochist and wouldn't probably want to do that manually for each event, it's better to add some logic to a separate event component. You can define custom components for virtually everything in the view by sticking the components prop into the calendar component, like so:

<BigCalendar
    components={{
      eventWrapper: EventWrapper
    }}
    ...
  />

The custom EventWrapper component would then look like this:

/* EventWrapper.js */

...

export default const EventWrapper = ({ event, children }) => {
  const { title, className } = children.props;
  const customClass = `${className} rbc-event--${event.type}`;
  const hourStart = moment(event.start).hour();
  const hourStop = moment(event.end).hour();
  const gridRowStart = hourStart + 1;

  return (
    <div
      title={title}
      className={customClass}
      style={{ gridRow: `${gridRowStart - 0} / span ${hourStop - hourStart}` }}
    >
      {children.props.children}
    </div>
  );
};
  • hourStart and hourStop store the start and end hours, respectively
  • gridRowStart represents the line where the event item begins
  • the item then spans across hourStop - hourStart grid tracks
  • a custom class is added for handling styles of different types of events
  • the original style object with values generated by the library is completely ditched (sorry man)

Here's a super exciting colorful version of the calendar with the latest updates:

calendar-view-2

If you play with the date of the events, you can see how nicely the grid lays itself out without overlapping. Sweeeeet!

Here's the sandbox with the whole code: https://codesandbox.io/s/zqk8vmqn3m

Some other hints and notes:

  • You can also calculate a gridRowStop for more control (in certain cases using span wouldn't work as desired)
  • If you need to pass stuff to EventWrapper or any other custom component, you can wrap it with a HOC, like so eventWrapper: withProps(EventWrapper, { cow: moo })
  • The possibilities are endless

Resources:

Show Comments