No Time Dad

A blog about web development written by a busy dad

Responsive Grid Area Dashboard

I went down the twelve column grid rabbit hole. I think it’s a good approach for structuring pages, but it can become tedious quickly. Figuring out where to start and stop each grid-column can make my head spin. I feel like I get lost, and I pitty the poor soul who has to look over my code and make changes later.

Most of the big web frameworks like Bootstrap or Material implement some form of the twelve column grid layout. The grid is generally initialized in css as follows:

.grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 10px;
}

.item1 {
  border: 2px dashed red;
  border-radius: 0.5rem;
  padding: 1rem;
  grid-column: 1 / span 6;
}

.item2 {
  border: 2px dashed blue;
  border-radius: 0.5rem;
  padding: 1rem;
  grid-column: 7 / span 6 ;
}
<div class="grid">
  <div class="item1">1</div>
  <div class="item2">2</div>
</div>
1
2

The .grid selector creates the twelve columns that occupy an equal amount of space via a single fractal unit 1fr. I’d then have two items that are on the same row, splitting the twelve columns equally. If I open chrome dev tools (F12) and inspect the element, I can see the twelve grid sections defined even though they aren’t visible on the rendered page itself.

dev_tools_grid

Again, this 12 column grid layout is very powerful. But, I still find it overly complex. I think a better solution, especially for making dashboards, is to use grid-template-areas. I can then label each area and lay it out as needed. I don’t have to define start and stop points for each selector.

What’s interesting is that recent versions of Bootstrap use flexbox instead of grid to define their twelve column grid. They do this by defining utility classes for various numbers of columns using percentages instead of span declarations. I think this is done to avoid some awkward spacing issues and orphaned columns on some screen sizes.

The main idea with grid-template-areas is that I can define my rows and columns with names that correspond to selectors. In the example below, I define a grid via grid-template-areas that has 2 columns and three rows. The header will span the width of both columns and one row, the nav will span the width of one column and two rows, and main will span the width the one column and two rows.

The 2x3 grid is pretty much visualized for me in the grid-template-areas value:

grid-template-areas: 
    "head head"
    "nav  main"
    "nav  main";

Full example:

<section class="page">
  <header>Header</header>
  <nav>Navigation</nav>
  <main>Main area</main>
</section>
.page {
  display: grid;
  grid-template-areas: 
    "head head"
    "nav  main"
    "nav  main";
  grid-template-rows: 50px 1fr 1fr;
  grid-template-columns: 150px 1fr;
}

.page > header {
  grid-area: head;
}

.page > nav {
  grid-area: nav;
}

.page > main {
  grid-area: main;
}

Building a dashboard layout

So, what if I wanted to use grid-template-areas to create a responsive dashboard layout? Well, I’ll start with the mobile view, which will be a single column. I want a few of the sections to span multiple rows, so I’ll just repeat their area names.

The full example can be viewed here.

/* dashboard.module.css */
.Content {
  padding: 1rem;
  display: grid;
  grid-auto-rows: 200px;
  /* Define a single column for small screens */
  grid-template-areas:
    "a"
    "b"
    "c"
    "d"
    "e"
    "e"
    "f"
    "f"
    "g"
    "g";
  gap: 20px;
}

.Card {
  padding: 1rem;
  border-radius: 0.5rem;
  border: 3px dashed cornflowerblue;
}

/* Using nth-child here for convenience. Each div needs a grid-area name */
.Card:nth-child(1) {
  grid-area: a;
}

.Card:nth-child(2) {
  grid-area: b;
}

.Card:nth-child(3) {
  grid-area: c;
}

.Card:nth-child(4) {
  grid-area: d;
}

.Card:nth-child(5) {
  grid-area: e;
}

.Card:nth-child(6) {
  grid-area: f;
}

.Card:nth-child(7) {
  grid-area: g;
}
// Dashboard.jsx
import React from 'react';
import * as styles from './index.module.css';

const Index = () => (
  <div className={styles.Content}>
    <div className={`${styles.Card}`}>a</div>
    <div className={`${styles.Card}`}>b</div>
    <div className={`${styles.Card}`}>c</div>
    <div className={`${styles.Card}`}>d</div>
    <div className={`${styles.Card}`}>e</div>
    <div className={`${styles.Card}`}>f</div>
    <div className={`${styles.Card}`}>g</div>
  </div>
);

export default Index;
a
b
c
d
e
f
g

Then, for larger screens I’d just need to add a breakpoint and re-design my grid. In this example, I’m going to use a 4x4 grid with a few of the items spanning multiple columns and rows. The html remains the same, the only change required is in the css.

a
b
c
d
e
f
g
/* dashboard.module.css */
...
@media (min-width: 768px) {
  .Content {
    grid-template-areas:
      "a b c d"
      "e e e f"
      "e e e f"
      "g g g f";
  }
}
// Dashboard.jsx
import React from 'react';
import * as styles from './index.module.css';

const Index = () => (
  <div className={styles.Content}>
    <div className={`${styles.Card}`}>a</div>
    <div className={`${styles.Card}`}>b</div>
    <div className={`${styles.Card}`}>c</div>
    <div className={`${styles.Card}`}>d</div>
    <div className={`${styles.Card}`}>e</div>
    <div className={`${styles.Card}`}>f</div>
    <div className={`${styles.Card}`}>g</div>
  </div>
);

export default Index;

Conclusion

As always with css, there are many ways to achieve this responsive dashboard layout. It can absolutely be done using the twelve column grid layout, but I find the named sections much easier to read and understand.

It also seemed easier to make adjustments to the grid in the media queries. I think I’d need to adjust each div’s selector in the grid if I was using the 12 column grid method, which could get messy and error prone. Again, there’s probably an easier way here dev_tools_grid.png

Final code

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

/* dashboard.module.css */
.Header {
  padding: 0.5rem;
  background-color: aliceblue;
}

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

.Content {
  padding: 1rem;
  display: grid;
  grid-auto-rows: 200px;
  grid-template-areas:
    "a"
    "b"
    "c"
    "d"
    "e"
    "e"
    "f"
    "f"
    "g"
    "g";
  gap: 20px;
}

.Card {
  padding: 1rem;
  border-radius: 0.5rem;
  border: 3px dashed cornflowerblue;
}

.Card:nth-child(1) {
  grid-area: a;
}

.Card:nth-child(2) {
  grid-area: b;
}

.Card:nth-child(3) {
  grid-area: c;
}

.Card:nth-child(4) {
  grid-area: d;
}

.Card:nth-child(5) {
  grid-area: e;
}

.Card:nth-child(6) {
  grid-area: f;
}

.Card:nth-child(7) {
  grid-area: g;
}

@media (min-width: 768px) {
  .Content {
    grid-template-areas:
      "a b c d"
      "e e e f"
      "e e e f"
      "g g g f";
  }
}
// Dashboard.jsx
import React from 'react';
import * as styles from './index.module.css';

const Index = () => (
  <div>
    <div className={styles.Header}>
      <FireIcon />
    </div>
    <div className={styles.Content}>
      <div className={`${styles.Card}`}>a</div>
      <div className={`${styles.Card}`}>b</div>
      <div className={`${styles.Card}`}>c</div>
      <div className={`${styles.Card}`}>d</div>
      <div className={`${styles.Card}`}>e</div>
      <div className={`${styles.Card}`}>f</div>
      <div className={`${styles.Card}`}>g</div>
    </div>
  </div>
);

const FireIcon = () => (
  <svg xmlns="http://www.w3.org/2000/svg" className={styles.Header__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;