No Time Dad

A blog about web development written by a busy dad

Inbox Style Sidebar with CSS

I was browsing the Bootstrap examples page looking for some interesting layouts to try and I stumbled on their sidebars page. I’ve made more than a few sidebars in the past, and most of the examples on the page looked similar to what I’ve already done.

There was, however, one sidebar that caught my attention. It was one that looked like an email inbox. This was definitely not a style that I’ve tried before and I was curious how it was made. Below is a screenshot of what it looks like.

bootstrap_inbox

It consists of a header, and a sibling element that is scrollable. My first thought was that it was making use of position: fixed or position: absolute in the header and scrollable section. It turns out that Bootstrap was doing some interesting things with flexbox to make the scrollable div work.

To be honest, I couldn’t figure out how they were getting this to work with only flexbox. Which makes me wonder if there was another css class somewhere that I somehow wasn’t seeing, or if I just didn’t understand flexbox as deeply. Either way, I did eventually figure out how to get this to work without using flexbox.

The tricky thing about trying to replicate Bootstrap examples is that they use a lot of utility classes in their code and sometimes it’s tedious to unwind what they’re actually doing. The hardest part of this layout was the scrolling section. Aside from that, it’s really just a two column grid with some fancy stuff happening in the sidebar.

The full demo can be viewed here.

Building the inbox layout

The entire layout for the page is going to to be a grid. A two column grid to be exact. I’ll intialize the grid in a container div using display: grid and define the column sizes using grid-template-columns: 380px 1fr. Which will create one 380px wide column for the sidebar and use the remaining space as the second column for the content.

I rarely set exact pixel widths for elements, especially for sidebars. The reason I’m doing it here is because Bootstrap does it. Ok, that isn’t a great reason. The actual reason is that I’m going to use li elements for each item in the scrollable section, and it’s easier to work with the spacing of those elements if there’s an exact width set in the parent.

Basic layout

But, before I get to the scollable elements I need a basic layout to work from. It’s so easy for me to jump to designing inner elements before I even have the layout defined. This is a mistake that usually ends up biting me later.

sidebar
content
export const InboxComponent = () => (
  <div className={styles.Container}>
    <div className={styles.Sidebar}>sidebar</div>
    <div className={styles.Content}>content</div>
  </div>
);
.Container {
  display: grid;
  /* The sidebar will be 380px wide and the content will fill the remaining space */
  grid-template-columns: 380px 1fr;
}

.Sidebar {
  background-color: lightcoral;
}

.Content {
  background-color: lightgray;
}
Header

Now that I have my layout, I can start building the inner elements on the sidebar. Working from the top down, the first element will be the header. This element is also, in my opinion, the least complicated. The header features an icon and text in a flexbox container. Centering the two items is just a matter of using align-items: center once I had them in the flexbox container.

There is, however, one critical piece of the header that is crucial to the scrollable section. That is the header height. This is also where my version and Bootstrap’s version of this inbox styles sidebar begin to diverge. Mainly in the latter’s probable usage of flexbox to handle to the scrolling section.

In my version the sidebar needs its height as tall as possible. The obvious candidate for this is height: 100vh, but that introduces a new problem. The header is not included in the 100vh, so the entire page would have an extra scrollable space equal to whatever the height of the header is.

I suppose it isn’t the end of the world to have some extra space, but it bothered me enough that I spent a considerable amount of time trying to fix it. I’m sure there are some better solutions out there, but I decided to use a css custom property to hardcode the height of the header. I can then use that custom property to calculate the .Sidebar height using the css calc utility function. This removes the extra space.

So, I’ll modify the .Container selector slightly by adding the css custom property for the header height. I’ll also modify the .Sidebar selector and add the height property with the calc function to account for the header height.

Another important change I’ll make to the .Sidebar selector is to add overflow-y: auto. The overflow-y property tells the browser what to do when the content of a div exceeds its height. The auto value is nice because the browser will remove the scrollbar if my content is shorter than the defined height value.

export const InboxComponent = () => (
  <div className={styles.Container}>
    <div className={styles.Sidebar}>
      <div className={styles.Header}>
        ...
      </div>
    </div>
    <div className={styles.Content}>content</div>
  </div>
);
.Container {
  display: grid;
  grid-template-columns: 380px 1fr;
  /* Shared custom property to adjust for extra space */
  --header-height: 55px;
}

.Sidebar {
  /* Let the browser decide when to show/hide the scrollbar */
  overflow-y: auto;
  /* Account for the header height */
  height: calc(100vh - var(--header-height));
}

.Header {
  padding: 1rem;
  /* Use the hardcoded custom property to define the height */
  height: var(--header-height);
  display: flex;
  align-items: center;
  border-bottom: 1px solid lightgray;
}

.Content {
  background-color: lightgray;
}

I had a hard time with this implementation of the scrollable inbox. I knew what was happening, why it was happening, and how to fix it, but it just feels wrong to hardcode height values like this. This is not how Bootstrap handled this issue, but I couldn’t crack the code on how they were making this work using flexbox. I eventually just had to move on live with how it is. I honestly think that knowing when to move on from a problem you’re stuck on is an art. It’s difficult.

Inbox item

The inbox items themselves are li elements that sit inside of a ul. The tricky thing here was to make the entire li element clickable. I initially added padding directly to the li element which meant that I had a 1rem area around each item that was not a clickable link. I solved this problem by wrapping the content in a div that was inside of the anchor a element. Then I could add the padding and ensure that there was no space on the item that wasn’t clickable.

Aside from that, I used flexbox to vertically center the text and date. I also used the margin-left: auto trick on the date to push it all the way to the right. Lastly, a border at the bottom helps separate each item when there are multiple items.

const InboxItem = () => (
  <li className={styles.List_Item}>
    <a href="#item">
      <div className={styles.List_Item__Content}>
        <div className={styles.List_Item__Header}>
          <div className={styles.Header__Text}>List item header</div>
          <div className={styles.Header__Date}>Mon</div>
        </div>
        <div className={styles.List_Item__Text}>
          Some placeholder content in a paragraph below the heading and date.
        </div>
      </div>
    </a>
  </li>
);
.List_Item {

  text-decoration: none;
  border-bottom: 1px solid darkgrey;
}

.List_Item:hover {
  background-color: lightgray;
}

/* Placeholder for .List_Item__Active */
.List_Item:first-child {
  background-color: lightgray;
}

.List_Item__Content {
  padding: 1rem;
}

.List_Item__Header {
  display: flex;
  align-items: center;
  color: black;
}

.Header__Text {
  font-weight: 600;
}

.Header__Date {
  margin-left: auto;
  opacity: 0.6;
}

.List_Item__Text {
  color: black;
  padding-top: 0.5rem;
  font-size: 14px;
  opacity: 0.8;
}

It’s worth nothing that I cheated a bit when it came to highlighting the active list item. I used the first-child pseudo-class to modify the first element and assume it’s the active one. I did it this way because in my demo I use map to populate lots of items and I didn’t want to mess around with trying to pick a random one to declare as active. A better implementation when be to pick one from a list and pass it down through via props.

Conclusion

I made some concessions, but still ended up with a half decent version of Bootstrap’s inbox sidebar. This was the second Bootstrap example I’ve tried to recreate, and also the second time I’ve found it to be much more difficult than I expected. Which is a testament to the quality of that library I think. I’m still curious about their flexbox implementation of this inbox sidebar so I might investigate that further.

Final code and demo

The full demo can be viewed here. The fire svg icon is from heroicons.

// package.json
...
"react": "^17.0.2"
...
import React from 'react';
import * as styles from './index.module.css';

const Index = () => (
  <div className={styles.Container}>
    <div>
      <div className={styles.Header}>
        <FireSvg />
        <div>bored.io</div>
      </div>
      <div className={styles.Sidebar}>
        <ul>
          {[...Array(50)].map((_, i) => <InboxItem key={i} />)}
        </ul>
      </div>
    </div>
    <div className={styles.Content}></div>
  </div>
);

const InboxItem = () => (
  <li className={styles.List_Item}>
    <a href="#item">
      <div className={styles.List_Item__Content}>
        <div className={styles.List_Item__Header}>
          <div className={styles.Header__Text}>List item header</div>
          <div className={styles.Header__Date}>Mon</div>
        </div>
        <div className={styles.List_Item__Text}>
          Some placeholder content in a paragraph below the heading and date.
        </div>
      </div>
    </a>
  </li>
);

const FireSvg = () => (
  <svg xmlns="http://www.w3.org/2000/svg" className={styles.Icon} viewBox="0 0 20 20" fill="currentColor">
    <path fillRule="evenodd" d="M12.395 2.553a1 1 0 00-1.45-.385c-.345.23-.614.558-.822.88-.214.33-.403.713-.57 1.116-.334.804-.614 1.768-.84 2.734a31.365 31.365 0 00-.613 3.58 2.64 2.64 0 01-.945-1.067c-.328-.68-.398-1.534-.398-2.654A1 1 0 005.05 6.05 6.981 6.981 0 003 11a7 7 0 1011.95-4.95c-.592-.591-.98-.985-1.348-1.467-.363-.476-.724-1.063-1.207-2.03zM12.12 15.12A3 3 0 017 13s.879.5 2.5.5c0-1 .5-4 1.25-4.5.5 1 .786 1.293 1.371 1.879A2.99 2.99 0 0113 13a2.99 2.99 0 01-.879 2.121z" clipRule="evenodd" />
  </svg>
);

export default Index;
/* index.module.css */
.Container {
  display: grid;
  grid-template-columns: 380px 1fr;
  --header-height: 55px;
}

.Content {
  background-color: lightgrey;
}

.Sidebar {
  overflow-y: auto;
  height: calc(100vh - var(--header-height));
}

.Header {
  padding: 1rem;
  height: var(--header-height);
  display: flex;
  align-items: center;
  border-bottom: 1px solid lightgray;
}

.Icon {
  width: 28px;
  height: 28px;
}

.List_Item {
  /* Override gatsby globals */
  padding-left: 0;
  margin-bottom: 0;

  text-decoration: none;
  border-bottom: 1px solid darkgrey;
}

.List_Item:hover {
  background-color: lightgray;
}

/* Placeholder for .List_Item__Active */
.List_Item:first-child {
  background-color: lightgray;
}

.List_Item__Content {
  padding: 1rem;
}

.List_Item__Header {
  display: flex;
  align-items: center;
  color: black;
}

.Header__Text {
  font-weight: 600;
}

.Header__Date {
  margin-left: auto;
  opacity: 0.6;
}

.List_Item__Text {
  color: black;
  padding-top: 0.5rem;
  font-size: 14px;
  opacity: 0.8;
}