No Time Dad

A blog about web development written by a busy dad

Responsive Stacked Layout with CSS

I somehow keep finding my way back to Tailwindui when looking for design inspiration. They have great designs, what can I say? They’re all clean and professional, similar to what a real customer might be looking for.

While scrolling through Tailwindui’s components, I noticed they had a section called “stacked layouts” that caught my attention. I hadn’t heard of this term before, but the layouts looked like they were a top toolbar with a bottom content section, which is pretty common. I guess I just didn’t know it had an official name. Below is a screenshot of one of the layouts with a dark toolbar.

stacked_layout

So, I thought it would be fun to try and build something similar without using Tailwindcss, and instead with vanilla css. While this template will have some basic responsiveness, I really wanted to focus on the layout itself. I’m not going to implement a fancy drop-down menu or anything like that.

The full demo can be viewed here, and the final code can be viewed here.

Getting started

More and more, I start my layouts just using colored boxes. Once I get the boxes aligned how I like, I then go back and fill in the elements. I’ll do the same here. Since this is a “stacked” layout, I won’t need to initialize a grid or do anything special.

One thing that does get a litte tricky with this layout is that I want the content section (gray area in the screenshot above) to fill all of the available space on the page after the headers. Filling available space instantly makes me think of flexbox.

I’ll initialize flexbox on the parent container and set the flex-direction to column. I can then add min-height: 100vh to the parent container to ensure it takes up the entire height. Doing this will allow me to later pick a child div that I want to fill the space with via flex: 1. In this case, that child will be the content section.

Header
Content header
Content main
<div class="Container">
  <div class="Header">Header</div>
  <div class="Content">
    <div class="Content__Header">Content header</div>
    <div class="Content__Main">Content main</div>
  </div>
</div>
.Container {
  /* Init flexbox so child divs can fill available space */
  display: flex;
  flex-direction: column;
  /* Important! Cannot fill available space without this */
  min-height: 100vh;

  /* To help visualize layout, remove later */
  border: 1px solid red;
}

.Header {
  padding: 1rem;
  border: 1px solid blue;
}

.Content {
  /* This flexbox is what allows Content__Main to grow to fill the space */
  display: flex;
  flex-direction: column;
  /* Grow to fill the remaining space */
  flex: 1;
}

.Content__Header {
  padding: 1rem;
  border: 1px solid orange;
}

.Content__Main {
  /* Grow to fill the remaining space */
  flex: 1;
  padding: 1rem;
  background-color: lightgray;
}

Now I have a layout and I can start focusing on each individual section.

Header section

In the header I initialize flexbox to help with alignment and spacing. The header will consist of three elements. The app icon element, the nav links element, and the alert icon element. Some of these individually have their own child elements, but I find it easier to think of them as single elements until I get them aligned, similar to what I did for the overall layout.

Icon
Alert icon
<div class="Header">
  <div class="Header__Icon">Icon</div>
  <div class="Header__Nav_Links">Nav links</div>
  <div class="Header__Alert_Icon">Alert icon</div>
</div>
.Header {
  /* Initialize flexbox and space out each element */
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem;
  /* To help visualize layout, remove later */
  border: 1px solid orange;
}

.Header__Icon {
  padding: 1rem;
  border: 1px solid blue;
}

.Header__Nav_Links {
  padding: 1rem;
  border: 1px solid red;
}

.Header__Alert_Icon {
  padding: 1rem;
  border: 1px solid green;
}

The header is probably the most complicated part of this stacked layout. And I don’t think it was too bad. Flexbox does most of the work.

Responsiveness

There isn’t any content in this template, so I only had to worry about the nav menu on small screens. It wasn’t a huge surprise to me that the nav menu broke the layout on small screens. I decided the easiest thing to do for it was to show and had the links on the top toolbar based on the screen size using display: none and display: block in a breakpoint.

Friendly reminder that display: none removes the given element from the DOM

The main consideration I had to take here was where the menu should go on small screens. I ended up putting it at the top of the content div, then changing the background color to match the toolbar so it looks like it’s still part of the toolbar.

The other issue here was code duplication. I had to make a copy of the nav element for the content section so I could apply different styles to it and put it in two different divs. There are probably some clever ways to avoid the duplication, but I didn’t want to spend a ton of time on it.

Below is an abbreviated version of the nav and mobile nav selectors, as well as the breakpoint that toggles them.

.Header__Nav {
  /* Default to hidden on small screens */
  display: none;
}

.MobileNav {
  /* Default to visible on small screens */
  display: flex;
}

@media (min-width: 768px) {
  .Header__Nav {
    /* Display the header navigation on larger screens */
    display: block;
  }

  .MobileNav {
    /* Hide the mobile nav on larger screens */
    display: none;
  }
}

Conclusion

Sometimes I wish I learned the trick about colored divs for visualizing layouts earlier in my career. I feel like I really could’ve saved myself some time. I know Chrome and Firefox dev tools have tools to help see the element boundaries, but I find that just adding a border or coloring the background of an element is much quicker for me in the long run.

The hardest part for me was getting the content section to grow to fill the remaining space. But, I eventually figured out that I was missing a flex: 1 on the Content selector. Somehow, my bumbling persistance paid off.

Anyways, this was a fun layout. I sort of assumed it wasn’t going to be too complicated to build compared to some others I’ve done recently. Even if it wasn’t super challenging, it was fun.

Demo & final code

I wrote this example using React, mostly for convenience. I didn’t use state or any click handlers, though. So, it should be relatively easy to translate it to vanilla html or another framework as needed.

The full demo can be viewed here. The svg icons are 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}>
    <Header />
    <Content />
  </div>
);

const Header = () => (
  <div className={styles.Header}>
    <a href="#home">
      <FireSvg />
    </a>
    <nav className={styles.Header__Nav}>
      <a className={`${styles.Header__Nav_Link} ${styles.Header__Nav_Link__Active}`} href="">Dashboard</a>
      <a className={styles.Header__Nav_Link} href="">Team</a>
      <a className={styles.Header__Nav_Link} href="">Projects</a>
      <a className={styles.Header__Nav_Link} href="">Reports</a>
    </nav>
    <a href="#alerts"><BellSvg /></a>
  </div>
);

const MobileNav = () => (
  <nav className={styles.MobileNav}>
    <a className={`${styles.Header__Nav_Link} ${styles.Header__Nav_Link__Active}`} href="">Dashboard</a>
    <a className={styles.Header__Nav_Link} href="">Team</a>
    <a className={styles.Header__Nav_Link} href="">Projects</a>
    <a className={styles.Header__Nav_Link} href="">Reports</a>
  </nav>
)

const Content = () => (
  <div className={styles.Content}>
    <MobileNav />
    <div className={styles.Content__Header}>
      <div className={styles.Content__Header__Text}>Dashboard</div>
    </div>
    <div className={styles.Content__Main}>
      <div className={styles.Content__Main__Placeholder}></div>
    </div>
  </div>
);

const FireSvg = () => (
  <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>
);

const BellSvg = () => (
  <svg xmlns="http://www.w3.org/2000/svg" className={styles.Header__Right_Icon} fill="none" viewBox="0 0 24 24" stroke="currentColor">
    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
  </svg>
)

export default Index;
/* index.module.css */
.Container {
  --space-cadet: hsla(235, 21%, 21%, 1);
  --manatee: hsla(218, 17%, 62%, 1);
  --alice-blue: hsla(197, 24%, 94%, 1);
  --imperial-red: hsla(353, 86%, 54%, 1);
  --amaranth-red: hsla(350, 96%, 43%, 1);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.Header {
  padding: 1rem;
  background-color: var(--space-cadet);
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: var(--alice-blue);
}

.Header__Icon {
  height: 42px;
  width: 42px;
  color: var(--imperial-red);
}

.Header__Nav {
  display: none;
}

.Header__Nav_Link {
  text-decoration: none;
  color: var(--alice-blue);
  font-size: 16px;
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
}

.Header__Nav_Link__Active {
  border: 1px solid var(--manatee);
}

.Header__Nav_Link:hover:not(.Header__Nav_Link__Active) {
  opacity: 0.8;
}

.Header__Nav > .Header__Nav_Link:not(:first-child) {
  margin-left: 1rem;
}

.Header__Right_Icon {
  height: 28px;
  width: 28px;
  color: var(--manatee);
}

.Header__Right_Icon:hover {
  color: var(--alice-blue);
}

.Content {
  display: flex;
  flex-direction: column;
  flex: 1;
}

.MobileNav {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  background-color: var(--space-cadet);
}

.Content__Header {
  padding: 1rem;
  border-bottom: 1px solid var(--space-cadet);
}

.Content__Header__Text {
  font-size: 28px;
  font-weight: 800;
}

.Content__Main {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 1rem;
  padding-top: 2rem;
  padding-bottom: 2rem;
  background-color: var(--alice-blue);
}

.Content__Main__Placeholder {
  border: 6px dashed lightgray;
  border-radius: 1rem;
  flex: 1;
}

@media (min-width: 768px) {
  .Header__Nav {
    display: block;
  }

  .MobileNav {
    display: none;
  }
}