No Time Dad

A blog about web development written by a busy dad

Responsive Dashboard Tutorial - Part 2

Overview

Building on what we did in the last part of the tutorial, we are going to be focusing on the toolbar now. As I mentioned in part one of the tutorial, these tutorials are meant to be short (60 minutes or less). They are “bite sized” for busy people. With that in mind, we have three goals for this tutorial.

Goals
  1. Add alpinejs for the mobile menu
  2. Implement the mobile menu
  3. Improve the spacing on the toolbar

You might be wondering; what aplinejs is and why are we using it for the mobile menu? As their page describes, it is a small JavaScript library that is meant to add a “sprinkle” of JavaScript as needed. It is an alternative approach to using something like React or Vue, which can be quite big depending on what you’re doing.

So, why use alpine for the mobile menu? Well, as it turns out, dropdown menus can be sort of complicated to implement correctly. We’ll use alpine to control the state of the menu, as well as alpine’s built-in @click.away to handle when the menu should close.

Improving the Toolbar

Adding alpine

Adding alpinejs is pretty easy. We’ll simply add a new <script> tag to our header to pull the minified JavaScript file from alpine’s content delivery network (CDN). We are going to pull whatever is the latest version of alpine, but you might consider pinning a specific version for production.

In your index.html file, update the <head> section to include the <script> tag for alpinejs:

<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>
<!-- index.html -->
...
<head>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="./styles.css">
  <!-- New script tag for aplinejs -->
  <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>
  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap" rel="stylesheet">
  <title>Responsive Dashboard</title>
</head>
...
Creating the Mobile Menu State

The first thing we are going to want to do is add state to our mobile menu. We need to know when it should open and close. We’ll do this using alpine’s x-data attribute, which allows use to create a state object for a given html element. Our mobile menu is going to sit underneath of our toolbar in the content section and push the contents down when it opens. That means that we’ll want to define our state at a higher level than the toolbar so it can be accessed from the content block.

We’ll update the content div to include an x-data definition as shown below. Setting the open value to false means that the mobile menu will be closed when the page first loads.

<!-- index.html body -->
...
<body>
  <div class="container">
    <div class="sidenav text-bright">Sidenav</div>
    <!-- x-data attribute added to the content div -->
    <div class="content text-bright" x-data="{ open: false }">
      <div class="toolbar text-bright">Toolbar</div>
      Content
    </div>
  </div>
</body>
...
Adding Mobile Menu Buttons

Now that we are tracking the mobile menu state, we need a way to allow the user to toggle the menu open and closed. Basically, we need some buttons. Each of these buttons will update the { open: false } state that we defined in the previous step. We’ll use the classic hamburger icon when { open: false} and the menu is closed to open it, and we’ll use an X button when {open: true } to close the menu when it is open. The svg icons themselves are from heroicons.

Both of these buttons will be hidden on screens larger than 640px, so we’ll also be updating our media query definition to include a display: none; for the class we assign to the buttons.

First, we’ll update styles.css to include a new selector called toolbar__mobile_menu_button. In that selector we’ll set margin-left: auto, which will push the button all the way to the right because the toolbar itself is a flexbox. Additionally, we’ll update our media query to include the same toolbar__mobile_menu_button but only with display: none; defined, which will hide the button on larger screens. We’ll also add a selector to style the svg icons themselves.

/* styles.css */
...
/* Add the new selector for the mobile menu botton */
.toolbar__mobile_menu_button {
  margin-left: auto;
}

.toolbar__menu_icon {
  height: 20px;
  width: 20px;
}

/* Update the existing media query to include the new selector */
@media (min-width: 640px) {
  ...
  /* Mobile menu toggle buttons will be hidden on large screens */
  .toolbar__mobile_menu_button {
    display: none;
  }
}
...

Now we’ll add the actual buttons to our index.html file. Two important attributes to note here are x-show and @click, both of which come from alpinejs. The x-show attribute will toggle display: none; (hide) and display: block; (show) on a given element. As mentioned above, we want to show the hamburger icon when the menu is closed and we want to show the X icon when the menu is open. The @click attribute is a click handler from alpinejs that changes our state value when the button is clicked. On each click the current value of open in our state will be toggled to the opposite value. Below is the updated <body> tag from above.

...
<body>
  <div class="container">
    <div class="sidenav text-bright">Sidenav</div>
    <div class="content text-bright" x-data="{ open: false }">
      <div class="toolbar text-bright">
        Toolbar
        <!-- Hamburger icon that changes the open state to true when clicked -->
        <button class="toolbar__mobile_menu_button" x-show="!open" @click="open = true">
          <svg class="toolbar__menu_icon" class="" xmlns="http://www.w3.org/2000/svg" fill="none"
            viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
              d="M4 6h16M4 12h16M4 18h16" />
          </svg>
        </button>
        <!-- X icon that changes the open state to false when clicked -->
        <button class="toolbar__mobile_menu_button" x-show="open" @click="open = false">
          <svg class="toolbar__menu_icon" xmlns="http://www.w3.org/2000/svg" fill="none"
            viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
              d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
      </div>
      Content
    </div>
  </div>
</body>
...

At this point, if you’re updating the full code from the previous part of the tutorial, you should be able to open the dashboard in your local browser and see how the buttons behave. Clicking the buttons should toggle between the two, and expanding the viewport to larger than 640px should hide the buttons.

Creating the Actual Mobile Menu

The buttons are great and all, but we are really here for the menu. Users need to be able to navigate on our dashboard when on a mobile device. For that, we’ll add a new div in the content section with a class called mobile_menu. We are going to x-show again to toggle the display value (show/hide). We are going to be using a new attribute from alpinejs called @click.away, which will close the when the user has clicked on something else.

The @click.away attribute is one of the main reasons we are using alpinejs for the mobile menu. Implementing click away behavior is hard and alpinejs does it really well.

Our mobile menu is going to contain three links, but for our purposes here in this tutorial we are only going to be implementing the Dashboard page. The others are just placeholders for future implementations. The three links will be Dashboard, Analytics, and User. The analytics and user links were picked for no other reason than I needed some filler links and those two seemed like something other dashboard seemed to have. You can obviously swap them out for whatever pages you’d like to implement.

As for the structure of the mobile menu itself, the mobile menu is used for navigating, we are going to put the links in a nav tag. We’ll use an unordered list <ul> tag with list item <li> elements to contain the nav links themselves. We’ll want the entire <li> element to be a clickable link. A little trick for making an entire <li> clickable when it contains a link is to add display: block; to the <li> element.

As far as styling the mobile menu goes, we will want the background-color to match the sidenav and the toolbar, and we’ll want a way to visually show the user which link is the “active” link (or current page) by adding an “active” modifier to our nav link classes. Lastly, we’ll add a little more spice to the menu by adding a simple hover effect via the :hover modifier.

We’ll add four new selectors to our styles.css file, as shown below. Take extra care to ensure that nav_item_active comes after mobile_menu__nav_item since we will be adding it as a modifier. Otherwise it will be overwritten. We’ll also add a display: none; for the mobile_menu selector in our media query so that the menu is hidden on larger screens.

/* styles.css */
.mobile_menu {
  background-color: var(--oxford-blue);
}

.mobile_menu__nav_item {
  padding: 1rem;
  display: block;
}

.mobile_menu__nav_item:hover {
  background-color: var(--independence);
}

/* Active modifier must come after mobile_menu__nav_item */
.mobile_menu__nav_item_active {
  background-color: var(--independence);
}

@media (min-width: 640px) {
  ...
  /* Hide the menu on larger screens */
  .mobile_menu {
    display: none;
  }
}

Now we can update our index.html to include the mobile_menu block at the top of the content section of our dashboard.

<!-- index.html -->
<body>
  <div class="container">
    <div class="sidenav text-bright">Sidenav</div>
    <div class="content text-bright" x-data="{ open: false }">
      <div class="toolbar text-bright">
        <!-- Toolbar section omitted -->
      </div>
      <div class="mobile_menu" x-show="open" @click.away="open = false">
        <nav>
          <ul>
            <li>
              <a class="mobile_menu__nav_item mobile_menu__nav_item_active" href="#dashboard">Dashboard</a>
            </li>
            <li>
              <a class="mobile_menu__nav_item" href="#analytics">Analytics</a>
            </li>
            <li>
              <a class="mobile_menu__nav_item" href="#users">Users</a>
            </li>
          </ul>
        </nav>
      </div>
      Content
    </div>
  </div>
</body>

At this point you should be able to open up the dashboard in your local browser and toggle the mobile menu open and closed. The menu should hide if you expand the viewport past 640px, and the menu should close if you click on the content section of the dashboard. Lastly, all of our hover and active styles should be shown.

Improving the Spacing

Our toolbar is looking pretty good at this point, but I think we should make one more small change to improve it. That change is replacing the word “Toolbar” with “Dashboard” (since it is the active page), and increasing the font-size and font-weight in a new selector called toolbar__main_text to make it stand out a bit more. If you were adding more pages the names of those pages should replace the word “Dashboard” on the toolbar. It is basically a slot for the active page.

Another small change we’ll make at this point is to add a little padding to the toolbar element itself by adding padding: 1rem; to our existing toolbar selector. We’ll also want to ensure that the main text is centered vertically with the icon buttons. Since the toolbar itself is a flexbox, all we have to do is add align-items: center; to the toolbar selector.

/* styles.css */
.toolbar {
  display: flex;
  background-color: var(--oxford-blue);

  /* Adding two new properties to the existing selector */
  align-items: center;
  padding: 1rem;
}

/* Creating a new property for the main text on the toolbar */
.toolbar__main_text {
  font-size: 20px;
  font-weight: 700;
}
<body>
  <div class="container">
    <div class="sidenav text-bright">Sidenav</div>
    <div class="content text-bright" x-data="{ open: false }">
      <div class="toolbar text-bright">
        <!-- Removing the old Toolbar text with a new div -->
        <div class="toolbar__main_text">Dashboard</div>
        ...
      </div>
      <div class="mobile_menu" x-show="open" @click.away="open = false">
        <nav>
          ...
        </nav>
      </div>
      Content
    </div>
  </div>
</body>

Conclusion

Phew. Ok, that turned out to be a lot. Hopefully not too much for one session, but we did accomplish a ton. The toolbar is basically done at this point. We might add some more icons for the user later on, but for now I think it is good and we can move on the sidenav in the next tutorial.

Final Code
/* START RESETS */
...
/* END RESETS */

/* START DASHBOARD */
:root {
  --oxford-blue: #0b132bff;
  --space-cadet: #1c2541ff;
  --independence: #3a506bff;
  --maximum-blue-green: #5bc0beff;
  --turquoise-blue: #6fffe9ff;
}

.container {
  display: flex;
}

.sidenav {
  /* We want the sidenav to take up the entire height of the viewport */
  height: 100vh;
  /* Hide the sidenav on screens less than 640px */
  display: none;
  flex-direction: column;
  background-color: var(--oxford-blue);
}

@media (min-width: 640px) {
  .sidenav {
    display: flex;
    flex-direction: column;
  }

  .toolbar__mobile_menu_button {
    display: none;
  }

  .mobile_menu {
    display: none;
  }
}

.content {
  height: 100vh;
  display: flex;
  flex-direction: column;
  flex-grow: 1;
  background-color: var(--space-cadet);
}

.toolbar {
  display: flex;
  background-color: var(--oxford-blue);
  align-items: center;
  padding: 1rem;
}

.toolbar__menu_icon {
  height: 20px;
  width: 20px;
}

.toolbar__mobile_menu_button {
  margin-left: auto;
}

.mobile_menu {
  background-color: var(--oxford-blue);
}

.mobile_menu__nav_item {
  padding: 1rem;
  display: block;
}

.mobile_menu__nav_item:hover {
  background-color: var(--independence);
}

.mobile_menu__nav_item_active {
  background-color: var(--independence);
}

.toolbar__main_text {
  font-size: 20px;
  font-weight: 700;
}

.text-bright {
  color: var(--turquoise-blue);
}
/* END DASHBOARD */
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="./styles.css">
  <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js"
    defer></script>
  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap"
    rel="stylesheet">
  <title>Responsive Dashboard</title>
</head>

<body>
  <div class="container">
    <div class="sidenav text-bright">Sidenav</div>
    <div class="content text-bright" x-data="{ open: false }">
      <div class="toolbar text-bright">
        <div class="toolbar__main_text">Dashboard</div>
        <button class="toolbar__mobile_menu_button" x-show="!open" @click="open = true">
          <svg class="toolbar__menu_icon" class="" xmlns="http://www.w3.org/2000/svg" fill="none"
            viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
              d="M4 6h16M4 12h16M4 18h16" />
          </svg>
        </button>
        <button class="toolbar__mobile_menu_button" x-show="open" @click="open = false">
          <svg class="toolbar__menu_icon" xmlns="http://www.w3.org/2000/svg" fill="none"
            viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
              d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
      </div>
      <div class="mobile_menu" x-show="open" @click.away="open = false">
        <nav>
          <ul>
            <li>
              <a class="mobile_menu__nav_item mobile_menu__nav_item_active" href="#dashboard">Dashboard</a>
            </li>
            <li>
              <a class="mobile_menu__nav_item" href="#analytics">Analytics</a>
            </li>
            <li>
              <a class="mobile_menu__nav_item" href="#users">Users</a>
            </li>
          </ul>
        </nav>
      </div>
      Content
    </div>
  </div>
</body>

</html>