Building a Mobile-First Flexbox Hamburger Navigation

Posted .

The navigation we are building today may introduce some interesting concepts to you for the first time. A couple of newish technologies are being used here: Flexbox, BEM-style SCSS, and ES6/ES2015 JavaScript.

I am well-aware that these burger menus can be accomplished without any JavaScript via an <input type="checkbox" /> technique as demonstrated here. However, I would like to outline an approach leaning toward component-based architecture. Also, associating some JavaScript with this component will help us out in large-scale applications where state must be maintained and communicated throughout the application.

Adding the Markup

It’s easier to conceptualize the markup of a component first, since it outlines the structure and overall meaning. So let’s do that.

<!-- Main Navigation -->
<nav id="nav" class="nav" role="navigation">
  <div class="nav__container">
    
    <!-- Title -->
    <h1 class="nav__title">
      <a href="#" class="nav__title-link">Flexy Nav</a>
    </h1>
    
    <!-- Nav List -->
    <ul class="nav__list" role="menubar">
      <li class="nav__item">
        <a href="#" class="nav__link">Home</a>
      </li>
      <li class="nav__item">
        <a href="#" class="nav__link">Services</a>
      </li>
      <li class="nav__item">
        <a href="#" class="nav__link">About</a>
      </li>
      <li class="nav__item">
        <a href="#" class="nav__link">Contact</a>
      </li>
    </ul>
    
    <!-- Burger -->
    <a href="#" class="nav__burger"></a>

  </div> <!-- __container -->
</nav> <!-- .nav -->

As seen in the code above, BEM (Block Element Modifier) conventions are being used on the class names. Some frontend developers argue that this naming convention is ugly and rather confusing. I don’t think so really, especially because the benefits of this approach far outweight the cons in my opinion.

BEM naming convention facilitates the idea of component-based architecture. Styles for components can easily be “namespaced,” which makes it virtually impossible for another element or component style to conflict with one another. Good BEM implementation can also keep nested SASS selectors to a minimum because each element is named by its block level.

With the HTML in place, you should see results like below:

Picture of flex navigation results

If your results vary significantly, you should include normalize.css. Normalize.css applies sensible default styling to many HTML tags. It’s good practice to include CSS normalization because it will help maintain consistency between browsers.

Adding Some Style

Now let’s add some SCSS for our styles. You’ll either need to run SASS from the command line or run it from a task runner like Gulp. To make things easier, you can just use CodePen ;).

Boilerplate Styles

Since we know what colors we’ll be using let’s define those as SASS variables.

// --- Colors ---
$off-white: #eee;
$black: #191919;
$blue: #2AA3BF;
$cream: #e1e0dc;

Let’s also set the background color and default font.

@import 'https://fonts.googleapis.com/css?family=PT+Serif';

// --- Base Styles ---
body {
  font-family: 'PT Serif', serif;
  font-size: 20px;
  background-color: $cream;
  -webkit-font-smoothing: antialiased;
}

a {
  text-decoration: none;
}

Navigation Menu Styles

Now, it’s time for the real meat and potatoes of the styles. The SCSS below represents an approach for namespacing a component.

// --- Main Navigation ---

// Namespace (treat as a component)
$ns: '.nav';

// Component styles
#{$ns} {

  // Styles for this component will go here

}

We are storing the namespaced class, .nav into the variable $ns. There is a good reason for doing this. If we ever decide to rename the base class of this component, we can just change it from a single place without the need to go back and rename our nested SASS selectors. This should all make sense after you see more of the code.

In the code below, all of the styles needed for our mobile navigation have been added.

See the Pen Flexbox Hamburger Navigation (mobile-only) by Keenan Staffieri (@keenode) on CodePen.

Styles for elements that belong to our component make use of SASS ampersand (&) operator. For example:

.nav {
  &__container {
    // styles...
  }
}

/* Outputs:
  .nav__container {
    // styles...
  }
 */

In our code so far, if you click the burger, nothing happens. Although we do have navigation items, they are currently hidden from view because .nav__list has CSS properties: height: 0; visibility: hidden; opacity: 0;. Next, we’ll need to somehow get this navigation to open up when the burger is clicked or touched.

Adding Functionality to the Burger

This is where JavaScript can help us. We also will not be taking the easy and boring route by using jQuery. Today, we are going to architect this by employing ES6 classes and good ol’ standard JavaScript to handle our DOM events.

Let’s start off by creating a base Component class.

/**
  Base Component class
 */
class Component {
  
  constructor (selector) {
    this.baseClass = 'component'
    this.$self = document.querySelector(selector)
  }
  
  getElement (eleName) {
    return this.$self.querySelector(`.${this.baseClass}__${eleName}`)
  }
  
  addModifier (modifierName) {
    this.$self.classList.add(`${this.baseClass}--${modifierName}`)
  }
  
  removeModifier (modifierName) {
    this.$self.classList.remove(`${this.baseClass}--${modifierName}`)
  }
}

The purpose of this class is to provide helper methods to any class that extends it. Since we are using BEM naming conventions, these helper methods provide an easier way to manipulate child elements within our component. If you aren’t already familar with OOP (Object-Oriented Programming), I highly recommend you read up on it here.

Next, let’s create a class with the actual component code. The class FlexNav extends our base class, Component. The reason we made the Component class in the first place is so we could possibly extend future components from it, and all of these components could share functionality from the class Component.

/**
  FlexNav Component class
 */
class FlexNav extends Component {

  constructor (selector) {

    // Call parent constructor method
    super(selector)

    // Set some properties
    this.baseClass = 'nav'
    this.isOpen = false
    
    // Cache burger element selector
    this.$burger = this.getElement('burger')
    
    // Bind events
    this.$burger.addEventListener('click', (e) => { this.open(e) })
    this.$burger.addEventListener('touchend', (e) => { this.open(e) })
  }
  
  open (event) {
    event.preventDefault()

    this.isOpen = !this.isOpen
    
    if (this.isOpen) {
      this.addModifier('opened')
    } else {
      this.removeModifier('opened')
    }
  }
}

The constructor method is called when a new instance of this class is instantiated. In our constructor we call super() to call the parent class’ constructor. In our case, the Component constructor method is called first, where the selector parameter is passed and used to cache our selector into a variable for later use: this.$self = document.querySelector(selector).

When the burger is clicked, the open() method gets called because ‘click’ and ‘touchend’ events were bound to it from the constructor in FlexNav. If the navigation is currently NOT open, the modifier class .nav--opened is then added to the root component element .nav.

For us to visually see the navigation in its opened state, some modifier styles must be added:

// Component styles
#{$ns} {
  // element styles...
  
  // Nav Open Modifier state
  &--opened {
    // Fade in navigation list
    #{$ns}__list {
      height: 100vh;
      opacity: 1;
      transition: opacity 300ms ease-out;
    }

    // Animate burger
    #{$ns}__burger {
      &:before {
        transform: translate3d(0, 6px, 0) rotate(-45deg);
      }

      &:after {
        transform: translate3d(0, -6px, 0) rotate(45deg);
      }
    }
  }
  
  // element styles...
}

Modifier class, .nav--opened applies new styles to .nav__list and .nav__burger that cause the navigation items to fade into view and the burger to morph into an ‘X’.

With all of that set in place, nothing will work until after we instantiate the FlexNav class.

// Create FlexNav instance on document ready
document.addEventListener('DOMContentLoaded', function () {
  new FlexNav('#nav')
})

Everything for the mobile version of our navigation should be in working order now. The complete result is below:

See the Pen Flexbox Hamburger Navigation (mobile w/ functionality) by Keenan Staffieri (@keenode) on CodePen.

Adding CSS Media Queries for the Desktop Version

The desktop version of our navigation does not rely on any of our fancy ES6 JavaScript. We just have to add CSS media queries and new styles to some of our elements. Since we develop mobile-first, we’ll be using min-width media queries for the most part.

On desktop, our navigation container should be set to use flexbox so we can have more flexibility. We will also add justify-content: flex-end; to align all items toward the right.

// Nav Container
&__container {
  // mobile styles...

  @media screen and (min-width: 769px) {
    display: flex;
    justify-content: flex-end;
    margin: 0 auto;
    height: 100%;
    max-width: 800px;
  }
}

The navigation title has align-self: flex-start; so it can be constrained on the left side.

// Nav Title
&__title {
  // mobile styles...

  @media screen and (min-width: 769px) {
    flex: 1;
    align-self: flex-start;
    float: none;
    height: 100%;
  }
}

The navigation list is no longer absolutely positioned and is now fully opaque width display: flex;. We also reset the width and height properties.

// Nav List
&__list {
  // mobile styles...
    
  @media screen and (min-width: 769px) {
    display: flex;
    position: static;
    width: auto;
    height: auto;
    background-color: transparent;
    opacity: 1;
  }
}

The navigation item is set to display: flex; so we can vertically center the anchor with the align-items property.

// Nav List Item
&__item {    
  @media screen and (min-width: 769px) {
    display: flex;
    align-items: center;
    height: $nav-height;
  }
}

We add some padding and color transition effect on hover for the navigation links.

// Nav List Item Link
&__link {
  // mobile styles...
    
  @media screen and (min-width: 769px) {
    padding: 0 30px;
    transition: color 150ms ease-out;
      
    &:hover {
      color: $blue;
    }
  }
}

We then hide the burger element on desktop viewports.

// Nav Burger
&__burger {
  // mobile styles...
    
  @media screen and (min-width: 769px) {
    display: none;
  }
}

Lastly, we don’t want our ‘opened’ modifier styles to ever apply on desktop. Currently, if anyone decides to open the menu and THEN resize their browser beyond 768px, it will look broken. So let’s wrap those modifier styles inside a max-width media query.

// Nav Open Modifier state
&--opened {    
  @media screen and (max-width: 768px) {
    // mobile styles...
  }
}

And that’s it! The absolute final result is below. Some dummy content has been added to better-illustrate the fixed navigation.

See the Pen Flexbox ES6 Hamburger Navigation by Keenan Staffieri (@keenode) on CodePen.