CSS

Building a React Redux compatible hamburger menu – Custom implementation (part 2)


Reading Time: 6 minutes

Once you have completed the right Setup following the first part of these tutorials:
Building a React Redux compatible hamburger menus – Setup(part 1)

You’re ready to continue with the actual implementation.

Since we want to control when the burger menu is opened/closed using actions let’s start with the actual component and containers related to that

Let’s start by creating the src/components/, styles and src/containers/ folders:

mkdir src/components
mkdir src/containers
mkdir src/styles

Let’s continue by creating the HamburgerContainer in order to map the actions to be dispatched to component and show/hide the NavBarMenu

src/containers/HamburgerIconContainer.js

import { connect } from "react-redux";
import {
  hideMobileNavigationMenu,
  showMobileNavigationMenu
} from "../actions/BurgerAction";

import HamburgerIcon from "../components/HamburgerIcon/HamburgerIcon";

const mapStateToProps = (state, ownProps) => {
  return {
    ...ownProps
  }
};

const mapDispatchToProps = (dispatch) => {
  return {
    onHideMobileNavigationMenu: () => dispatch(hideMobileNavigationMenu()),
    onShowMobileNavigationMenu: () => dispatch(showMobileNavigationMenu())
  }
};

const HamburgerIconContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(HamburgerIcon);

export default HamburgerIconContainer;

Now, let’s declare the component HamburgerIcon

src/components/HamburgerIcon/HamburgerIcon.js

import React, { Component } from "react";
import "./HamburgerIcon.scss";

class HamburgerIcon extends Component{

  constructor(props) {
    super(props);
    this.onClickHandler = this.onClickHandler.bind(this);
  }

  onClickHandler() {

    const {
      showSideBarMenu,
      onShowMobileNavigationMenu,
      onHideMobileNavigationMenu
    } = this.props;

    showSideBarMenu ? onShowMobileNavigationMenu() : onHideMobileNavigationMenu();
  }

  render() {

    return(
      <div className="burger-container">
        <a className="burger-icon-link" onClick={this.onClickHandler} href="#">
          <span className="burger-span-element white-burger-icon-color"></span>
          <span className="burger-span-element white-burger-icon-color"></span>
          <span className="burger-span-element white-burger-icon-color"></span>
        </a>
      </div>
    );
  }
}

export default HamburgerIcon;

Now, we need to define our styles:

src/components/HamburgerIcon/HamburgerIcon.scss

@import "../../styles/responsiveness";

.burger-container {
  display: inline-block;
  height: 36px;
  margin-left: 10px;
  margin-top: 7px;
  width: 33px;

  @include mq-desktop {
    display: none;
  }
}

.burger-icon-link {
  display: inline-block;
  line-height: 8.5px;
  text-decoration: none;
  width: 33px;
}

.white-burger-icon-color {
  background-color: black !important;
}

.active-slide-panel {
  margin-left: 20px;
  margin-top: 19px;
}

.burger-span-element {
  display: inline-block;
  height: 3px;
  width: 30px;
}

We will use a couple of SASS files as utilities for media queries, mixin functions and variables:
For Media queries

src/styles/responsiveness.scss

@mixin mq-desktop($orientation: '') {
  @if $orientation == '' {
    @media (min-width: 1024px) { @content; }
  } @else {
    @media (min-width: 1024px) and (orientation: $orientation) { @content; }
  }
}

@mixin mq($max-width, $orientation: '') {
  @if $orientation == '' {
    @media (max-width: $max-width) { @content; }
  } @else {
    @media (max-width: $max-width) and (orientation: $orientation) { @content; }
  }
}

For variables

src/styles/variables.scss

$screen-xs-min:         480px;  // Extra small screen / phone
$screen-sm-min:         768px;  // Small screen / tablet
$screen-sm-max:         991px;  // Small screens / tablets
$screen-md-min:         992px;  // Medium screen / desktop
$screen-md-lg-min:      1100px; // Medium screen / desktop
$screen-lg-min:         1200px; // Large screen / wide desktop

For mixins

src/styles/mixins.scss

@mixin font-smoothing() {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

@mixin border-radius($radius) {
  -webkit-border-radius: $radius;
  -moz-border-radius: $radius;
  -ms-border-radius: $radius;
  border-radius: $radius;
}

@mixin transition($transition...) {
  -moz-transition: $transition;
  -o-transition: $transition;
  -webkit-transition: $transition;
  transition: $transition;
}

@mixin transform($transforms) {
  -moz-transform: $transforms;
  -o-transform: $transforms;
  -ms-transform: $transforms;
  -webkit-transform: $transforms;
  transform: $transforms;
}

@mixin translate3d($size, $x, $y) {
  @include transform(translate3d($size, $x, $y));
}

@mixin Opacity($value){
  $IEValue: $value*100;
  opacity: $value;
  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity="+$IEValue+")";
  filter: alpha(opacity=$IEValue);
}

and remember to update the main layout to include the hamburger icon

src/DefaultLayout.js

+ import HamburgerIconContainer from "./containers/HamburgerIconContainer.js";

      <div className="default-layout">
      + <HamburgerIconContainer showSideBarMenu />

        <div className="main-container">

If we followed every step correctly we should make the hamburger icon visible when you reduce the screen size

Now, it’s time to implement the actual sidebar menu
Let’s start by creating the container

src/containers/NavBarContainer.js

import { connect } from "react-redux";
import NavBar from "../components/NavBar/NavBar";
import {
  hideMobileNavigationMenu
} from "../actions/BurgerAction";

const mapStateToProps = (state, ownProps) => {
  return {
    visibleMobileMenu: state.BurgerMenuReducer.visibleMobileMenu,
    ...ownProps
  }
};

const mapDispatchToProps = (dispatch) => {
  return {
    onHideMobileNavigationMenu: () => dispatch(hideMobileNavigationMenu())
  }
};

const NavBarContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(NavBar);

export default NavBarContainer;

Since we want to hide the sidebar menu whenever we navigate to a new section we need to map that action to our component.
Such component would look like this:

src/components/NavBar/NavBar.js

import React, { Component } from "react";
import "./NavBar.scss";

import NavBarItem from "./NavBarItem";

class NavBar extends Component {

  constructor(props){
    super(props);
    this.handleMouseDown = this.handleMouseDown.bind(this);
  }

  handleMouseDown(e) {
    this.props.onHideMobileNavigationMenu();
    e.stopPropagation();
  }

  render() {

    let {
      visibleMobileMenu,
      onHideMobileNavigationMenu
    } = this.props;

    const navBarClass = visibleMobileMenu ? "show-menu" : "hide-menu";

    return (
      <div>
        <div onMouseDown={this.handleMouseDown} className={`overlay-container ${navBarClass}`}></div>
        <div
          className={`sidebar-container ${navBarClass}`}>
          <aside className="row">

            <ul className="sidebar">
              <NavBarItem
                path="/about"
                onHideMobileNavigationMenu={onHideMobileNavigationMenu}
                linkText="About us"
              />

              <NavBarItem
                path="/contact"
                onHideMobileNavigationMenu={onHideMobileNavigationMenu}
                linkText="Contact us"
               />
            </ul>
          </aside>
        </div>
      </div>
    );
  }
}

export default NavBar;

NOTE: one of the important parts in this component is:

<div onMouseDown={this.handleMouseDown} className={`overlay-container ${navBarClass}`}></div>

Since this element is responsible of hiding the Sidebar menu when it’s clicked (outside the sidebar menu container elements), it’s necessary to dispatch the same action to hide the menu.

The CSS for this component is:

src/components/NavBar/NavBar.scss

@import "../../styles/responsiveness";
@import "../../styles/variables";
@import "../../styles/mixins";

.sidebar{
  list-style: none;
  padding: 0;
  border: none;
  margin: 0;
  clear: both;

  @include mq($screen-sm-max) {
    padding-left: 18px;
  }

  li {
    padding: 20px 0 20px 0;
    text-align: center;
    margin: 0 auto;
    color: #FFFFFF;
    font-size: 10px;

    @include mq($screen-sm-max) {
      font-size: 16px;
      padding-left: 12px;
      padding: 0px;
    }

    &.disabled{
      color: #1F1F1F;
    }

    &.active{
      a{
        color: #FFA800;
        text-decoration: none;
      }
    }
    &.disabled {
      a {
        color: black;
        cursor: default;
        &:hover {
          color: black;
        }
      }
    }

    a {
      margin: 0px;
      padding: 0px;
      text-decoration: none;
      color: #FFFFFF;
      &:hover{
        color: #FFA800;
        text-decoration: none;
      }
    }

  }
}

.sidebar-container {
  background: #555555;
  z-index: 99;

  .sidebar {
    margin: 0;
    list-style: none;
    padding: 0;

    li {
      padding: 20px 0 20px 0;
      text-align: center;
      margin: 0 auto;
      color: #555555;
      font-size: 15px;
    }

    li a {
      color: white;
    }

    li.disabled {
      color: gray;
    }

    li a:hover {
      color: white;
      text-decoration: none;
    }
  }
}

.hamburger-container {
  display: none;
  padding: 0;
}

@include mq-desktop {
  .sidebar-container {
    height: auto;

    .sidebar {
      padding: 0 10px;

      li {
        color: #555555;
        display: inline-block;
        font-size: 15px;
        margin-right: 22px;
      }
    }
  }
}

@include mq($screen-sm-max) {

  .hamburger-container {
    display: block;
  }

  .overlay-container {
    @include transition(opacity 0.3s);
    @include Opacity(0);
    background: rgba(0, 0, 0, 0.4);
    bottom: 0;
    font-family: Arial, Helvetica, sans-serif;
    left: 0;
    position: fixed;
    right: 0;
    top: 0;
    z-index: 999;

    &.hide-menu {
      @include translate3d(100%, 0, 0);
      @include transition(opacity 0.3s, transform 0s 0.3s);
    }

    &.show-menu {
      @include Opacity(1);
    }
  }

  .logo-container {
    margin: 0 auto;
    width: 100%;
  }

  .sidebar-container {
    @include transition(all 0.3s cubic-bezier(0, 0.52, 0, 1));
    height: 100vh;
    left: 0;
    overflow: scroll;
    position: fixed;
    top: 0;
    width: 250px;
    z-index: 1000;

    &.hide-menu {
      @include translate3d(-300px, 0, 0);
    }

    &.show-menu {
      @include translate3d(0px, 0, 0);
      overflow: hidden;
    }
  }
}

Before moving on, I would like to explain the most important parts and elements of the CSS properties we just added above:

.overlay-container

That div is responsible of showing the dark overlay when the sidebar nav menu is open and is able to be closed by clicking on any area of the overlay container.

.sidebar-container

Here, we can find all the CSS effects to show the slide in and out effects using CSS3

If you were to look into the NavBar component you would see that we’ve used another NavBarItem component for the internal elements, that component looks like this:

src/components/NavBar/NavBarItem.js

import React, { Component } from "react";
import { Link } from "react-router-dom";

class NavBarItem extends Component {

  constructor(props) {
    super(props);
    this.onClickHandler = this.onClickHandler.bind(this);
  }

  getPath() {
    return this.props.path;
  }

  onClickHandler() {
    const { onHideMobileNavigationMenu } = this.props;
    onHideMobileNavigationMenu && onHideMobileNavigationMenu();
  }

  render() {
    const { className, linkText } = this.props;
    return (
      <li className={className}>
        <Link to={this.getPath()} onClick={this.onClickHandler}>
          {linkText}
        </Link>
      </li>
    );
  }
}

export default NavBarItem;

The final step is to include the NavBarContiner into the main layout

src/DefaultLayout.js

import NavBarContainer from "./containers/NavBarContainer.js";

src/DefaultLayout.js

+ import NavBarContainer from "./containers/NavBarContainer.js";

      <div className="default-layout">
       <HamburgerIconContainer showSideBarMenu />

      +  <NavBarContainer/>
        <div className="main-container">

The full file looks like this:

import React from 'react';
import HamburgerIconContainer from "./containers/HamburgerIconContainer.js";
import NavBarContainer from "./containers/NavBarContainer.js";
import { Route } from "react-router-dom";
import "./DefaultLayout.scss";

const DefaultLayout = ({component: Component, ...rest}) => {

  return (
    <Route {...rest} render={matchProps => (
      <div className="default-layout">
        <HamburgerIconContainer showSideBarMenu />

        <NavBarContainer />

        <div className="main-container">
          <Component {...matchProps} />
        </div>
        <div className="layout-footer">Footer</div>
      </div>
    )} />
  )
};

export default DefaultLayout;

This is how it should look at this point:

final example hamburger menu with sidebar for small devices

final example hamburger menu with sidebar for small devices

If you want to see the full example please visit the following pull request
https://github.com/heridev/react-burger-menu-implementations/pull/1/files

Or the direct branch called approach-from-scratch

That’s all I have for now, hope you find it useful, and stay tuned for the next part if you want to use an existent library to implement the same exact hamburger menu.

H.

Keep reading, coding and relaxing

Javascript
Rails 5 meets Webpack and React
CSS
CSS Selectors
Best Practices
8 Tips For Bootstrap