Calendar with CSS Grid

Laying out events on a calendar can be somewhat tricky. Many years ago, I had a client project where I needed to do a calendar similar to Google Calendar. I ended up using bitwise operators to figure out if there was room to put an event on a particular line. If there’s no room, it placed the event on the next available line.

There’s a lot of looping involved just to place an event on the calendar.

With CSS Grids, I wondered if I could take advantage of its auto-placement to accomplish the same thing.

The HTML

Of course, the first thing to worry about is the HTML. How should I mark up my events?

Here’s what I ended up going with:

<div class="week">
  <div class="day">
    <h3 class="day-label">1</h3>
    <div class="event event-start event-end" 
         data-span="2">Class</div>
    <div class="event event-end">Interview</div>
  </div>
</div>

Each week has its own container and each day has its own container. Within each day, we have a heading and then a list of events.

Why not use an actual list? A definite possibility. I tend to only go with a list if I think I’m going to normally have more than a couple items. I don’t want to overload people with too much information. (But this is conjecture. I should probably ask a bunch of people who use screen-readers.)

The CSS

I make each week a grid with each day taking an equal amount of space.

.week {
  display:grid;
  grid-template-columns: repeat(7, 1fr);
  grid-auto-flow: dense;
  grid-gap: 2px 10px;
}

Each week is its own grid because I need all the events to naturally flow into new rows. If the entire month was one big grid, it’d be difficult to flow events within a single week.

With Grid, we have a parent element that declares the grid and the child elements that are part of the grid. In this case, that would be the .day. We don’t want that part of the grid. We want its contents to be part of the grid. Therefore, I set the day to display:contents.

Unfortunately, display:contents only works in Chrome (with experimental features turned on) and Firefox. Womp womp. If we wanted to broaden support, we’d have to remove our day wrappers. (This would work fine since they don’t really do anything anyways.)

Getting Days on the First Row

The day numbers should all be on the first row, so we specify that with grid-row-start.

.day-label { 
    grid-row-start: 1;
}

Now, because we specified grid-auto-flow: dense; on our grid, all of the events will just find a spot where it can fit. Without that, each event is placed after the one before it, regardless of whether there’s room somewhere earlier in the grid.

And not a lick of JavaScript to do this!

Spanning Days

The next piece to this is having an event span over multiple days. I opted to use a parameter to do this. Thus, anytime an event needs to span more than a day, a data-span attribute is added to the element. The CSS then uses the span # syntax to span over a number of grid cells.

[data-span="1"] { grid-column-end: span 1; }

Keep in mind that if an event spans beyond the bounds of your 7 column grid, it’ll create new columns to fit in. (This’ll likely create some display side effects that you don’t want.)

James Finley went further and used CSS Custom Properties in his version.

.event {
  --span: 1;
  background-color: #CCC;
  grid-column-end: span var(--span);
}

Doing this, there’s less code in the CSS. In the HTML, you now need to specify an inline style instead of the data attribute.

<div class="event" style="--span:2;">

Start Day

Now that we have the events spanning the correct number of days, we also need the events to display as starting on the right day. The grid auto-placement will try to just find the first available box but what we really want is the first available box that starts on the right day of the week.

For this, I chose to use nth-child since the day wrapper will let the selector still work.

// “1” is the left-most edge, so the first day of the week
.day:nth-child(1) > .event { grid-column-start: 1; }

Without the day wrapper, you’d need to find another way to tell the events what day of the week to start on. Either by adding a class to each event or by making some assumptions in our HTML.

For example, if we know that day labels are always h3 headings and the events for that day come after it, then we can use nth-of-type to do the same thing.

h3:nth-of-type(1) ~ .event { grid-column-start: 1; }
h3:nth-of-type(2) ~ .event { grid-column-start: 2; }
...

Essentially we’re saying that any events that come after the first heading start on day 1. Any events that come after the second heading start on day 2. Due to the weight of the specificity, these styles need to be declared in order of the days of the week.

Other Calendar Considerations

The other thing that I cared about was styling events to indicate that they spanned over multiple weeks. Each week has their own events. That means that an event that starts on Friday of one week and goes to Monday of the following week would be two events. One 2 day event that displays on Friday to Saturday and another 2 day event that displays on Sunday to Monday.

(If your calendar starts on a Monday instead of a Sunday then, of course, you’d have a 3 day event and a 1 day event. Same problem!)

I add an event-end class to indicate the event ends in the current week. That rounds off the right-side corners of the event. I add an event-start class to indicate that the event starts in the current week. That rounds off the left-side corners of the event.

Check it out

Now that I’ve blathered on, go check it out!

Published December 08, 2017
Categorized as HTML and CSS
Short URL: https://snook.ca/s/1107