No Time Dad

A blog about web development written by a busy dad

Floating Sidenav

Intro

How many times can I make the same page layout? A lot, it turns out. When I think about it, the sidenav section plus content section is one of the most popular layouts. Especially if your page is a dashboard.

I sometimes browse dribbble for inspiration. There is always something interesting there I hadn’t thought about doing before. Which is helpful because I’m starting to grow tired of writing different variations of the same layout.

The other day I stumbled on a dashboard on where the sidenav was floating off to the side of the page. Typically, sidenavs are anchored to the side of the page. Now, I don’t think this layout is groundbreaking but it was a little different what I’ve been making recently, so I thought it would be fun to create my own version of it. Below is a screen shot of the design.

floating-sidenav

My version is not nearly as nice or complete, but I like it and it was fun to make. It’ll be a great template to build on later. The version I made isn’t responsive. I’ll update that another time, but for now it is best viewed on tablet or larger sized screens.

I really like simplicity. Lately, I have been taking the approach with my css that if I feel like a certain style is getting complicated I stop and rip it out. I realize this sounds silly and not practical, but I’ve been trying to keep my css files as minimal as possible while still achieving a nice design.

Initial page layout

The first thing I thought I should do is establish the sidenav and content sections of the page using css grid. The idea is that the page will consist of two columns. One for the sidenav and one for the page content.

I like to let the items in sidenav determine its column width. The content section can then just fill the remaining space on the page. Whenever I’m working on a layout I always add different color borders or backgrounds to elements. I know that most dev tools will show me the grid tracks, but for some reason I always end up just using colors.

This is a great starting point for any sidenav layout.

.Wrapper {
  /* Init grid layout  */
  display: grid;
  /* The first column allows the contents to determine its width. 
     The second column takes all available space for its width.  */
  grid-template-columns: auto 1fr;
  /* Ensure that the sidenav spans entire height of page */
  height: 100vh;
}

.Sidenav {
  /* Border added for visual reference */
  border: 1px solid lightcoral;
}

.Content {
  /* Border added for visual reference */
  border: 1px solid lightblue;
}
import React from 'react';
import * as styles from './floating-sidenav.module.css';

export const Wrapper = () => (
  <div className={styles.Wrapper}>
    <Sidenav />
    <Wrapper />
  </div>
);

const Sidenav = () => (
  <div className={styles.Sidenav}>Sidenav</div>
);

const Content = () => (
  <div className={styles.Content}>Content</div>
);
Sidenav
Content

Floating sidenav

It took me more time than I’d like to admit to figure out how to build the floating sidenav. My first few approaches involved adding margin and padding to the Sidenav component directly, but I kept having spacing issues.

What I ended up doing is creating a child div inside of the parent Sidenav component and adding margin to it instead. This gave me the look I wanted where the sidenav was pulled away from the edges of the parent div, giving it the floating effect. I then realized I could give it even more of a floating effect by adding a box-shadow.

Menu
Content
const Sidenav = () => (
  <div className={styles.Sidenav}>
    <div className={styles.Nav__Menu}>Menu</div>
  </div>
);
...
.Sidenav {
  /* Force the inner child to take up the full height */
  display: flex;
  /* Border added for visual reference */
  border: 1px solid lightcoral;
}

.Nav__Menu {
  /* Pull the menu away from the edges of the parent container */
  margin: 1rem;
  
  /* Init flexbox to help with alignment */
  display: flex;
  flex-direction: column;
  align-items: center;

  padding: 1rem;
  border-radius: 1rem;
  /* Emphasize floating effect */
  box-shadow: 0 10px 1rem lightgray;
  background-color: lightcoral;
}
...

Designing the menu

At this point I just needed to decide how I wanted the nav menu to look. I went the traditional large icon at the top and nav icons below look, but I think the menu is flexible enough for a lot of different designs.

I used flexbox a lot. Specifically, flex-direction: column; in more than a few places, since the menu is actually a grid column. In fact, I ended up using a second flexbox containter inside of .Nav__Menu to have better alignment control over the links themselves.

I’m just using text links here, but in my demo version I added the svg icons from heroicons. and additional styles to handle hovering and active state for the links.

...
.Nav__Links {
  padding-top: 1rem;
  display: flex;
  flex-direction: column;
}

.Nav__Link {
  text-decoration: none;
  color: white;
}
...
const Sidenav = () => (
  <div className={styles.Sidenav}>
    <div className={styles.Nav__Menu}>
      <a href="#link" className={styles.Nav__Link}>Header</a>
      <div className={styles.Nav__Links}>
        <a href="#link" className={styles.Nav__Link}>Link 1</a>
        <a href="#link" className={styles.Nav__Link}>Link 2</a>
        <a href="#link" className={styles.Nav__Link}>Link 3</a>
        <a href="#link" className={styles.Nav__Link}>Link 4</a>
      </div>
    </div>
  </div>
);

Content section

When I started working on this project I planned to leave the content section blank. I had no intention of implementing any sort of grid layout with cards. I just wanted to focus on the sidenav. But my curiosity got the better of me, and I started tinkering with it.

Out of all of the time I spent working on this demo, probably 80% of it was spent on the content section. Which is a little sad. But I think I landed on a good layout and I learned a lot about css grid.

I’ll be completely up front and say that the content section grid layout was hard for me. Much harder than I thought it was going to be. I probably went through ten different versions of it, and at one point I was just randomly changing grid-column values hoping I’d land on the layout I wanted by sheer luck. Definitely not my finest hour.

I did finally manage to acheive the layout I was looking for. I think the main issue was that I was trying to force more grid rows than I had defined. For example, I had three rows defined in grid-template-rows but for some reason I was trying to split one of the rows in half using some combination of span on the grid-column property. That turned out to be a bad idea, and what I needed to do was add a fourth row instead.

What I ended up with is a grid with 3 columns and 4 rows. The top row is where I put the name of the active page, “Dashboard” in my example. The other sections are open to interpretation but I envisioned the second row being stat cards, the third row being a larger pie chart with more stats to the right of it, and the fourth row being a line graph.

const Content = () => (
  <div className={styles.Content}>
    <div className={`${styles.Content__Section} ${styles.Section__Top}`}>Top</div>
    <div className={`${styles.Content__Section} ${styles.Section__A}`}>A</div>
    <div className={`${styles.Content__Section} ${styles.Section__B}`}>B</div>
    <div className={`${styles.Content__Section} ${styles.Section__C}`}>C</div>
    <div className={`${styles.Content__Section} ${styles.Section__D}`}>D</div>
    <div className={`${styles.Content__Section} ${styles.Section__E}`}>E</div>
    <div className={`${styles.Content__Section} ${styles.Section__F}`}>F</div>
  </div>
);
.Content {
  /* Initialized the content grid */
  display: grid;
  /* 4 rows */
  grid-template-rows: auto 1fr 150px 1fr;
  /* 3 columns */
  grid-template-columns: repeat(3, 1fr);
  /* Padding a spacing should match the sidenav */
  padding: 1rem;
  gap: 1rem;
}

.Content__Section {
  /* Generic styles for all content sections */
  border-radius: 1rem;
  padding: 1rem;
  border: 1px solid lightcoral;
}

.Section__Top {
  /* Top row should span all three columns and the height is determined by the it's contents */
  grid-column: span 3;
}

.Section__A {
  /* Defaults to column and row size, so this is a placeholder selector */
}

.Section__B {
  /* Defaults to column and row size, so this is a placeholder selector */
}

.Section__C {
  /* Defaults to column and row size, so this is a placeholder selector */

}

.Section__D {
  /* Span 2 columns and have a row height of 150px */
  grid-column: span 2;
}

.Section__E {
  /* Defaults to column and row size, so this is a placeholder selector */
}

.Section__F {
  /* Span 3 columns and take available row height */
  grid-column: span 3;
}

Conclusion & code

So, those were the hardest parts of building this floating sidenav dashboard. This is the main scaffolding, and the rest is filling in the styles. I eventually did remove the borders.

Scaffolding code

Below is the full code for the floating sidenav scaffolding described in this post.

import React from 'react';
import * as styles from './floating-sidenav.module.css';

export const Wrapper = () => (
  <div className={styles.Wrapper}>
    <Sidenav />
    <Wrapper />
  </div>
);

const Sidenav = () => (
  <div className={styles.Sidenav}>
    <div className={styles.Nav__Menu}>
      <a href="#link" className={styles.Nav__Link}>Header</a>
      <div className={styles.Nav__Links}>
        <a href="#link" className={styles.Nav__Link}>Link 1</a>
        <a href="#link" className={styles.Nav__Link}>Link 2</a>
        <a href="#link" className={styles.Nav__Link}>Link 3</a>
        <a href="#link" className={styles.Nav__Link}>Link 4</a>
      </div>
    </div>
  </div>
);

const Content = () => (
  <div className={styles.Content}>
    <div className={`${styles.Content__Section} ${styles.Section__Top}`}>Top</div>
    <div className={`${styles.Content__Section} ${styles.Section__A}`}>A</div>
    <div className={`${styles.Content__Section} ${styles.Section__B}`}>B</div>
    <div className={`${styles.Content__Section} ${styles.Section__C}`}>C</div>
    <div className={`${styles.Content__Section} ${styles.Section__D}`}>D</div>
    <div className={`${styles.Content__Section} ${styles.Section__E}`}>E</div>
    <div className={`${styles.Content__Section} ${styles.Section__F}`}>F</div>
  </div>
);

.Wrapper {
  display: grid;
  grid-template-columns: auto 1fr;
  height: 100vh;
}


.Sidenav {
  display: flex;
  border: 1px solid lightcoral;
}

.Nav__Menu {
  margin: 1rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 1rem;
  border-radius: 1rem;
  box-shadow: 0 10px 1rem lightgray;
  background-color: lightcoral;
}

.Nav__Links {
  padding-top: 1rem;
  display: flex;
  flex-direction: column;
}

.Nav__Link {
  text-decoration: none;
  color: white;
}


.Content {
  display: grid;
  grid-template-rows: auto 1fr 150px 1fr;
  grid-template-columns: repeat(3, 1fr);
  padding: 1rem;
  gap: 1rem;
}

.Content__Section {
  border-radius: 1rem;
  padding: 1rem;
  border: 1px solid lightcoral;
}

.Section__Top {
  grid-column: span 3;
}

.Section__D {
  grid-column: span 2;
}

.Section__F {
  grid-column: span 3;
}
Demo code

Below is the code for my example version with more icons and colors. Again, it is not responsive. Sorry mobile users.

import React from 'react';
import * as styles from './index.module.css';

const Index = () => (
  <div className={styles.Wrapper}>
    <Sidebar />
    <Content />
  </div>
);

const Sidebar = () => (
  <div className={styles.Sidebar}>
    <div className={styles.Sidebar__Nav}>
      <a href="#home" className={styles.Nav__Link} aria-label="Home"><FireSvg /></a>
      <div className={styles.Nav__Links}>
        <a className={`${styles.Nav__Link} ${styles.Icon__Active}`} href="#link" aria-label="Chart"><ChartBarSvg /></a>
        <a className={styles.Nav__Link} href="#link" aria-label="Activities"><ClipboardListSvg /></a>
        <a className={styles.Nav__Link} href="#link" aria-label="Friends"><UsersSvg /></a>
      </div>
    </div>
  </div>
);

const Content = () => (
  <div className={styles.Content}>
    <div className={`${styles.Content__Section} ${styles.Section__Top}`}>Dashboard</div>
    <div className={`${styles.Content__Section} ${styles.Section__A}`}>A</div>
    <div className={`${styles.Content__Section} ${styles.Section__B}`}>B</div>
    <div className={`${styles.Content__Section} ${styles.Section__C}`}>C</div>
    <div className={`${styles.Content__Section} ${styles.Section__D}`}>D</div>
    <div className={`${styles.Content__Section} ${styles.Section__E}`}>E</div>
    <div className={`${styles.Content__Section} ${styles.Section__F}`}>F</div>
  </div>
);



/* SVG Icons **/
const ChartBarSvg = () => (
  <svg className={styles.Icon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
    <path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z" />
  </svg>
);

const UsersSvg = () => (
  <svg className={styles.Icon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
    <path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
  </svg>
);

const ClipboardListSvg = () => (
  <svg className={styles.Icon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
    <path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
    <path fillRule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clipRule="evenodd" />
  </svg>
);

const FireSvg = () => (
  <svg className={`${styles.Icon} ${styles.Icon__Large}`} xmlns="http://www.w3.org/2000/svg" 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;
.Wrapper {
  /* Color palette */
  --bg: #f7f4eaff;
  --sidenav: #75c9c8ff;
  --content: #68a3fa;

  /* Init the grid */
  display: grid;
  grid-template-columns: auto 1fr;
  height: 100vh;
  background-color: var(--bg);
}

.Sidebar {
  /* Use flex to force full height */
  display: flex;
  color: var(--bg);
}

.Sidebar__Nav {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 1rem;
  padding: 1rem;
  background-color: var(--sidenav);
  border-radius: 1rem;
  box-shadow: 0 10px 1rem lightgray;
}

.Nav__Links {
  padding-top: 1rem;
  display: flex;
  flex-direction: column;
}

.Nav__Link {
  text-decoration: none;
  color: inherit;
}

.Icon {
  margin: 8px 8px 2px 8px;
  width: 2rem;
  height: 2rem;
}

.Icon__Large {
  width: 3rem;
  height: 3rem;
  margin-top: 1px;
}

.Icon__Active {
  border: 1px solid var(--bg);
  border-radius: 0.5rem;
}

.Icon:hover {
  opacity: 0.8;
}

.Content {
  display: grid;
  grid-template-rows: auto 1fr 350px 1fr;
  grid-template-columns: repeat(3, 1fr);
  padding: 1rem;
  gap: 2rem;
}

.Content__Section {
  border-radius: 1rem;
  color: var(--bg);
  padding: 1rem;
  box-shadow: 0 10px 1rem lightgray;
}

.Section__Top {
  grid-column: span 3;
  display: flex;
  align-items: center;
  padding: 0;
  margin-top: 1rem;
  color: var(--sidenav);
  font-size: 30px;
  font-weight: 700;
  box-shadow: none;
  border-radius: 0;
  border-bottom: 1px solid var(--sidenav);
}

.Section__A {
  background-color: var(--content);
}

.Section__B {
  background-color: var(--content);
}

.Section__C {
  background-color: var(--content);
}

.Section__D {
  grid-column: span 2;
  background-color: var(--content);
}

.Section__E {
  background-color: var(--content);
}

.Section__F {
  grid-column: span 3;
  background-color: var(--content);
}