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;
/* 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;
}
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})
}
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>
) receivesonClick={() => 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>
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' })
}
}
}
}
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>
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)
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.