DEV Community

Cover image for Interactive Navigation in React SPA
CiaraMaria
CiaraMaria

Posted on • Updated on

Interactive Navigation in React SPA

I wanted a way to show a top navigation bar in the hero section and a hamburger drop-down navigation as a user scrolls down the rest of the page as part of my React single page application.

After a lot of Googling along with some trial and error, I put together a few ideas to achieve my desired end result which looks like this:

Getting Started

I used:

  • react-scroll npm i react-scroll
  • react-burger-menu npm i react-burger-menu
  • Two separate components: one for top nav and one for the drop-down nav

Setting Up Burger Menu

Instructions for burger set up and customization are here. Here is what my Sidenav.js and Sidenav.css files looked like after tweaking the default:

import React, {Component} from 'react';
import './Sidenav.css';
import { slide as Menu } from 'react-burger-menu';
import {Link} from 'react-scroll';

class Sidenav extends Component {

  render () {

    return (
      <div className='sidenav'>
        <Menu width={ '15%' } >

          <Link className='nav-li' style={{ cursor: 
           "pointer"}} to="home" spy={true} smooth= 
           {true}>Home</Link>

          <Link className='nav-li' style={{ cursor: 
           "pointer"}} to="about" spy={true} smooth= 
           {true}>About</Link>

          <Link className='nav-li' style={{ cursor: 
           "pointer"}} to="work" spy={true} smooth= 
           {true}>Work</Link>

          <Link className='nav-li' style={{ cursor: 
           "pointer"}} to="contact" spy={true} smooth= 
           {true}>Contact</Link>

          <a className='nav-li' target='_blank' rel="noopener 
           noreferrer" href='linktoyourblog'>Blog</a>

        </Menu>    
      </div>
    );
  }
}

export default Sidenav;
Enter fullscreen mode Exit fullscreen mode
/* Position and sizing of burger button */
.bm-burger-button {
  position: fixed;
  width: 30px;
  height: 25px;
  left: 10px;
  top: 36px;
}

/* Color/shape of burger icon bars */
.bm-burger-bars {
  background-color: #EFC6BA;
}


/* Position and sizing of clickable cross button */
.bm-cross-button {
  height: 24px;
  width: 24px;
}

/* Color/shape of close button cross */
.bm-cross {
  background: #222222;
}

/*
Sidebar wrapper styles
Note: Beware of modifying this element as it can break the animations - you should not need to touch it in most cases
*/
.bm-menu-wrap {
  position: fixed;
  height: 100%;
}

/* General sidebar styles */
.bm-menu {
  background: #FFFFFF;
  padding: 2.5em 0.5em 0;
  font-size: 1.15em;
}

/* Morph shape necessary with bubble or elastic */
.bm-morph-shape {
  fill: #FFFFFF;
}

/* Wrapper for item list */
.bm-item-list {
  color: #FFFFFF;
  padding: 0.8em;
}

/* Individual item */
.bm-item {
  display: inline-block;
}

/* Styling of overlay */
.bm-overlay {
  background: #FFFFFF;
}

/* Styling of links */
.nav-li {
  font-family: 'Playfair Display';
  color: #000000;
  outline: none;
  padding: 10%;
  text-decoration: underline;
  text-transform: uppercase;
}

/* Removes purple outline when clicked */
.nav-li:focus {
  outline: none;
}

.nav-li:hover {
  text-decoration: none;
  font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

Managing State

At this point, the burger menu is working. There are additional state and event handlers available which are useful. By adding the following above render(), the side menu will close when a link is clicked rather than having to click on the 'X':


    state = {
      menuOpen: false,
    }

    handleStateChange (state) {
      this.setState({menuOpen: state.isOpen})  
    }

    // closes menu on link click
    closeMenu () {
      this.setState({menuOpen: false})
    }
Enter fullscreen mode Exit fullscreen mode

Next, I added props to the Menu and Link components in return().

  • Menu receives isOpen={this.state.menuOpen} onStateChange={(state) => this.handleStateChange(state)}
  • Link (and <a>) receives onClick={() => this.closeMenu()}
<Menu width={ '15%' } isOpen={this.state.menuOpen} 
  onStateChange={(state) => this.handleStateChange(state)}>

 <Link className='nav-li' style={{ cursor: "pointer"}} 
  to="home" spy={true} smooth={true} onClick={() => 
  this.closeMenu()}>Home</Link>

 <Link className='nav-li' style={{ cursor: "pointer"}} 
  to="about" spy={true} smooth={true} onClick={() => 
  this.closeMenu()}>About</Link>

 <Link className='nav-li' style={{ cursor: "pointer"}} 
  to="work" spy={true} smooth={true} onClick={() => 
  this.closeMenu()}>Work</Link>

 <Link className='nav-li' style={{ cursor: "pointer"}} 
  to="contact" spy={true} smooth={true} onClick={() => 
  this.closeMenu()}>Contact</Link>

 <a className='nav-li' target='_blank' rel="noopener 
  noreferrer" href='linktoyourblog' onClick={() => 
  this.closeMenu()}>Blog</a>

</Menu>  
Enter fullscreen mode Exit fullscreen mode

Alright! The burger menu is fully functional now.

So... how do we only make it visible as a user scrolls away from the landing view?

componentDidMount() and onscroll events

componendDidMount() is invoked immediately after a component is mounted. As this happens we will watch for a scroll event to initiate a state change which changes the <div> opacity.

In Sidenav.js:

 state = {
      menuOpen: false,
      opacity: '0'
    }

    // will show sidenav when scroll position is above 500
    componentDidMount() {
      if (typeof window !== 'undefined') {
        window.onscroll = () => {
          let currentScrollPos = window.pageYOffset;
          let maxScroll = document.body.scrollHeight - 
          window.innerHeight;
          if (currentScrollPos < 500 && currentScrollPos < 
           maxScroll) { 
            this.setState({ opacity: '0'})
            // to get position: console.log(currentScrollPos)
           } else {
            this.setState({ opacity: '1' })
          }
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

What's happening here? I'm using state and inline CSS styling to control the visibility of the drop-down menu. I added opacity to our state and give it a default value of 0 so that it does not display on page load. In the function, I use setState to change the opacity based on the current scroll position of the page.

Let's go back to return(). All of the content should be wrapped in a <div className='sidenav>, to which we add inline styling:

<div className='sidenav' style={{ opacity: `${this.state.opacity}`}}>

...

</div>
Enter fullscreen mode Exit fullscreen mode

That's it! Now the drop-down is only visible after a certain point on the page view.

Conclusion & Addressing Performance

A somewhat hodgepodge interactive navigation design for SPA's.

After doing more research on componentDidMount() and onscroll events, I noticed some concerns about performance issues using this type of approach. In its current usage, this isn't completely killing my app's performance.

Upon running a Lighthouse audit I get:

Although, that is a drop from the previous 97 on Performance and 100 on Best Practices. 👀

That being said, I'd love to have a discussion on performance or perhaps a more efficient way to implement this interactive navigation that I'm not aware of-- so if you have something to teach me, drop a comment below! 💭

Top comments (1)

Collapse
 
ronca85 profile image
ronca85

Don't you think people should resist google and their ranking of everything? We're basically bowing down to some machine, basically a deity. We should't care what it has to say. If you made a website you're happy with and it works that should be the end of it.