Responsive calendar layout with flexbox
Tags: css
Published: 2020-01-28 20:45:27

For the better part of two decades, I've been running [openmikes.org][omo], a directory of open mikes[^1], jam nights, etc. across the US and Canada. As with many hobby sites, especially of this vintage, it's showing its age.

I was particularly annoyed with the [calendar][cal] page. Although it's probably the *worst* way to display this data in terms of scannability and useful-information density, it's also the most popular. I've tried killing it, I've tried hiding it... either way, I get complaints.

But there's no longer any excuse to display a squished desktop calendar on mobile devices. I wanted to restructure the layout so that it would collapse to a list on narrower screens. But the old code was a mess - all sorts of "off day" padding logic to place the first day of the month on the right day of the week (in the *table* I was using for layout), empty days that really shouldn't show up in a list view, etc.

I took a first cut at a new layout using CSS Grid, but a small percentage of my users are still on IE 10 or 11. No real grid support, and this layout includes corner cases that the polyfills can't handle. So, [Flexbox][flex] it is.

My goals:

- I wanted to keep the HTML simple, and let CSS do the heavy lifting (with some math help from [SASS][sass])
- JavaScript shouldn't be necessary
- I wanted to start the calendar HTML off on the 1st of the month, with no "padding" cells
- No fill-out-the-week padding at the end of the month, either.

What I *was* willing to include in the HTML:

- Each day's "cell" has a "calday" class.
- Each cell is also tagged with its day of the week, so we have `class="calday sunday"`, `class="calday monday"`, etc.
- Days with no events also get an "emptyday" class.
- Days that have already passed get a "calpast" class

The first few days of February 2019, therefore, might look like this:

```html
<div class="calheader">
  <div class="calday">Sunday</div>
  <div class="calday">Monday</div>
  <div class="calday">Tuesday</div>
  <div class="calday">Wednesday</div>
  <div class="calday">Thursday</div>
  <div class="calday">Friday</div>
  <div class="calday">Saturday</div>
</div>
<div class="calendar" id="calendar">
  <div class="calday friday"><span class="caldate">1</span>
    <ul>
      <li>words</li>
      <li>words</li>
      <li>words</li>
    </ul>
  </div>
  <div class="calday saturday"><span class="caldate">2</span>
    <ul>
      <li>words</li>
      <li>words</li>
      <li>words</li>
      <li>words</li>
    </ul>
  </div>
  <div class="calday sunday emptyday"><span class="caldate">3</span>
    <ul></ul>
  </div>
  <div class="calday monday"><span class="caldate">4</span>
    <ul>
      <li>words</li>
      <li>words</li>
      <li>words</li>
    </ul>
  </div>
  <!-- and so on -->
</div>
```

Going mobile-first, we:

- Let each day take 100%  width
- Hide "emptyday" and "calpast" cells
- Hide the "calheader" section which labels the day-of-week columns in full-sized views.

```css
.calheader,
.calpast,
.emptyday {
  display: none;
}

.calday {
  width: 100%;
}
```

That's the default view in this CodePen:

{% codepen https://codepen.io/paulroub/pen/RvjgNw %}

On wider screens (at least 55em, adjustable via the `$calbreak` SASS variable), we:

- Bring the "calheader" section back
- Restore empty days and past days
- Set each calendar day to 1/7th of the available width
- Use Flexbox layout in the calendar and its header

```scss
$cellwidth: (100% / 7);
$calbreak: 55em;

$cellwidth:
@media screen and (min-width: $calbreak) {
  .calendar,
  .calheader {
    display: flex;
    flex-flow: row wrap;
    justify-content: space-around;
  }

  .calpast,
  .emptyday {
    display: block;
  }

  .calday {
    width: $cellwidth;
  }
}
```

(SASS is not strictly necessary here, but I'd rather let the tool do the calculations for me, for correctness and readability's sake)

If that's all we did, every month would start in the first column -- i.e. every month would start on Sunday. Historically, I'd either add empty cells before the first of the month (1 for Monday, 2 for Tuesday, etc.), and perhaps something similar at the end to maintain the last week's cell spacing. That's annoying, and adds more junk that we need to hide in mobile views.

So each cell also gets a "sunday", "monday", etc. class. And we let CSS (again, via SASS) do something special with that information:

```scss
@mixin day($dayOfWeek) {
  @media screen and (min-width: $calbreak) {
    &:first-child {
      margin-left: ($cellwidth * ($dayOfWeek - 1));
    }

    &:last-child {
      margin-right: ($cellwidth * (7 - $dayOfWeek));
    }
  }
}

.sunday {
  @include day(1);
}
.monday {
  @include day(2);
}
.tuesday {
  @include day(3);
}
.wednesday {
  @include day(4);
}
.thursday {
  @include day(5);
}
.friday {
  @include day(6);
}
.saturday {
  @include day(7);
}
```

Assume the first day of the month is on a Tuesday. That "tuesday" class compiles to:

```scss
@media screen and (min-width: 55em) {
  .tuesday:first-child {
    margin-left: 28.5714285714%;
  }
  .tuesday:last-child {
    margin-right: 57.1428571429%;
  }
}
```

Meaning: on wide-enough displays, if this Tuesday is the first day in the list (*i.e.* the first day of the month), add a left margin of 2/7th of the screen — the same as two calendar cells.

Similarly, if the last day of the month is a Tuesday, right-pad it with 4-cells' width of space.

Now:

- Each week will span exactly one row (because each cell is 1/7th of the width)
- Flexbox will keep the heights of the cells in a row the same, without any extra CSS needed
- Our list will start in the correct column.

You can reduce the size of the preview in the above CodePen, or [view it full size][preview] to see this more clearly. Resize the window to watch the break happen.

The JavaScript in the CodePen is there to fill out the calendar for a given month, with semi-random content, and add the appropriate classes so you can see — for example — the lack of empty days in mobile view.

Feedback, suggestions, improvements are very much welcome.


[^1]:  "Um, actually, it's 'mic'". Oh, thanks. You may be surprised to find [this has come up before](https://openmikes.org/faq/#mikedammit).
[omo]: https://openmikes.org/
[preview]: https://codepen.io/paulroub/full/RvjgNw
[cal]: https://openmikes.org/calendar/
[flex]: https://css-tricks.com/snippets/css/a-guide-to-flexbox/ "A Complete Guide to Flexbox"
[sass]: https://sass-lang.com/ "SASS: Syntactically Awesome Style Sheets"