How to make a sliding navigation menu
This tutorial is about making the navigation menu from Developer Bacon. This article is aimed at people that don't know Vue.js and has been stripped out of this tutorial. There is the use of SCSS but there will be examples with both SCSS and the compiled CSS. The JavaScript for this tutorial uses the library AnimeJs. If you want to just look at the code you can find it in the Codepen example below or continue this article for the examples.
The HTML
Let's get started with the HTML for the navigation menu. The HTML example has all the necessary tags necessary for both the CSS and Js script to look the same as Developer Bacon. These can, of course, be changed to whatever you like (just make sure that you change it both in the CSS file and the JS file).
<header class="header">
<a class="logo-link" href="#">
Developer Bacon
</a>
<nav class="nav">
<a class="nav__link" href="#">Search</a>
<a class="nav__link" href="#">About</a>
<a class="nav__link" href="#">Contact</a>
</nav>
<div class="nav__background nav__background-1"></div>
<div class="nav__background nav__background-2"></div>
<button class="nav-ham toggleNav">
<span class="nav-ham__line"></span>
<span class="nav-ham__line"></span>
<span class="nav-ham__line"></span>
</button>
<div class="close-nav toggleNav"></div>
</header>
The SCSS and CSS
This first example is the SCSS for the navigation menu. If you are not familiar with SCSS the second example is the compiled SCSS to CSS.
// Based on this [snippet](https://css-tricks.com/snippets/sass/mixin-manage-breakpoints/) by Hugo Giraudel.
@mixin break($point) {
@if map-has-key($breakpoints, $point) {
@media screen and (max-width: map-get($breakpoints, $point)) {
@content;
}
} @else {
@warn 'Unfortunately, no value could be retrieved from `#{$breakpoint}`. '
+ 'Available breakpoints are: #{map-keys($breakpoints)}.';
}
}
$breakpoints: (
"sm": 800px,
"md": 1000px
) !default;
.header,
.header * {
box-sizing: border-box;
}
body {
background-color: black;
}
nav[role="navigation"] {
text-align: center;
a {
display: inline-block;
margin: 1em 0.75em 2em;
}
}
.header {
background-color: red;
display: flex;
justify-content: space-between;
margin-bottom: 20px;
padding: 1rem;
box-shadow: 0 2px 10px 1px rgba(0, 0, 0, 0.2);
}
img.logo-link {
height: 1rem;
width: auto;
}
.logo-link,
.nav__link {
text-decoration: none;
text-transform: uppercase;
letter-spacing: 1px;
color: black;
transition: color 0.3s ease;
&:hover,
&:focus {
text-decoration: none;
color: white;
}
}
.nav__link {
margin-left: 20px;
display: inline;
@include break(md) {
padding: 0.75rem;
}
}
.nav-ham,
.nav__background {
display: none;
}
.nav-ham {
z-index: 2;
}
@include break(md) {
.nav {
display: none;
&.is-active {
display: flex;
flex-direction: column;
padding: 8rem 2rem 70vh 1rem;
justify-content: space-around;
text-align: right;
position: fixed;
background-color: #222;
color: white;
top: 0;
height: 100vh;
width: 15rem;
z-index: 5;
box-shadow: 0 2px 10px 1px rgba(0, 0, 0, 0.2);
a {
color: white;
border-color: white;
}
}
}
.nav__background-1,
.nav__background-2 {
&.is-active {
display: block;
z-index: 3;
background-color: gray;
box-shadow: 0 2px 10px 1px rgba(0, 0, 0, 0.2);
top: 0;
height: 100vh;
width: 18rem;
position: fixed;
}
}
.nav-ham {
display: block;
background: none;
border: none;
outline: none;
height: 3rem;
width: 3rem;
position: absolute;
right: 1rem;
top: 0;
z-index: 6;
.nav-ham__line {
border: 1px solid white;
height: 0;
width: 2rem;
display: block;
margin: 0.6rem 0 0.6rem auto;
transform-origin: center right;
transition: 0.3s;
}
& .is-active {
&:first-child {
transform-origin: center right;
transform: rotate(-45deg);
}
&:nth-of-type(2) {
width: 0;
opacity: 0;
}
&:last-child {
transform-origin: center right;
transform: rotate(45deg);
}
}
}
}
.close-nav {
display: none;
position: fixed;
right: -100vw;
top: 0;
background: rgba(0, 0, 0, 0.7);
height: 100vh;
width: 100vw;
opacity: 0;
z-index: 2;
@include break(md) {
&.is-active {
display: block;
}
}
}
.logo {
vertical-align: bottom;
width: 33px;
padding: 0 1px;
}
The compiled CSS
This is the compiled CSS from the SCSS above.
.header,
.header * {
box-sizing: border-box;
}
body {
background-color: black;
}
nav[role="navigation"] {
text-align: center;
}
nav[role="navigation"] a {
display: inline-block;
margin: 1em 0.75em 2em;
}
.header {
background-color: red;
display: -webkit-box;
display: flex;
-webkit-box-pack: justify;
justify-content: space-between;
margin-bottom: 20px;
padding: 1rem;
box-shadow: 0 2px 10px 1px rgba(0, 0, 0, 0.2);
}
img.logo-link {
height: 1rem;
width: auto;
}
.logo-link,
.nav__link {
text-decoration: none;
text-transform: uppercase;
letter-spacing: 1px;
color: black;
-webkit-transition: color 0.3s ease;
transition: color 0.3s ease;
}
.logo-link:hover,
.logo-link:focus,
.nav__link:hover,
.nav__link:focus {
text-decoration: none;
color: white;
}
.nav__link {
margin-left: 20px;
display: inline;
}
@media screen and (max-width: 1000px) {
.nav__link {
padding: 0.75rem;
}
}
.nav-ham,
.nav__background {
display: none;
}
.nav-ham {
z-index: 2;
}
@media screen and (max-width: 1000px) {
.nav {
display: none;
}
.nav.is-active {
display: -webkit-box;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex-direction: column;
padding: 8rem 2rem 70vh 1rem;
justify-content: space-around;
text-align: right;
position: fixed;
background-color: #222;
color: white;
top: 0;
height: 100vh;
width: 15rem;
z-index: 5;
box-shadow: 0 2px 10px 1px rgba(0, 0, 0, 0.2);
}
.nav.is-active a {
color: white;
border-color: white;
}
.nav__background-1.is-active,
.nav__background-2.is-active {
display: block;
z-index: 3;
background-color: gray;
box-shadow: 0 2px 10px 1px rgba(0, 0, 0, 0.2);
top: 0;
height: 100vh;
width: 18rem;
position: fixed;
}
.nav-ham {
display: block;
background: none;
border: none;
outline: none;
height: 3rem;
width: 3rem;
position: absolute;
right: 1rem;
top: 0;
z-index: 6;
}
.nav-ham .nav-ham__line {
border: 1px solid white;
height: 0;
width: 2rem;
display: block;
margin: 0.6rem 0 0.6rem auto;
-webkit-transform-origin: center right;
transform-origin: center right;
-webkit-transition: 0.3s;
transition: 0.3s;
}
.nav-ham .is-active:first-child {
-webkit-transform-origin: center right;
transform-origin: center right;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.nav-ham .is-active:nth-of-type(2) {
width: 0;
opacity: 0;
}
.nav-ham .is-active:last-child {
-webkit-transform-origin: center right;
transform-origin: center right;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
}
.close-nav {
display: none;
position: fixed;
right: -100vw;
top: 0;
background: rgba(0, 0, 0, 0.7);
height: 100vh;
width: 100vw;
opacity: 0;
z-index: 2;
}
@media screen and (max-width: 1000px) {
.close-nav.is-active {
display: block;
}
}
.logo {
vertical-align: bottom;
width: 33px;
padding: 0 1px;
}
The magic JavaScript
Before we get into the Javascript of this project we should add AnimeJs to the project. To do this use this command in your project directory.
yarn add animejs
I used AnimeJs because it is great for handling animations with Javascript. The AnimeJs documentation can be found at animejs.com.
document.querySelectorAll(".toggleNav").forEach(item => {
item.addEventListener("click", function() {
console.log("clicked");
toggleNav();
});
});
function closeNav() {
document.querySelectorAll(".nav-ham, .nav-ham span").forEach(function(item) {
item.classList.remove("is-active");
});
anime({
targets: ".nav",
duration: 400,
easing: "easeInOutSine",
right: [0, "-15rem"],
complete: function() {
document.querySelector(".nav").classList.remove("is-active");
}
});
anime({
targets: ".nav__background",
delay: 150,
duration: 400,
easing: "easeInOutSine",
right: ["0rem", "-18rem"],
complete: function() {
document.querySelectorAll(".nav__background").forEach(item => {
item.classList.remove("is-active");
});
}
});
anime({
targets: ".close-nav",
delay: 150,
duration: 400,
easing: "easeInOutSine",
right: [0, "-100vw"],
opacity: 0,
complete: function() {
document.querySelector(".close-nav").classList.remove("is-active");
}
});
}
function toggleNav() {
document.querySelectorAll(".nav-ham, .nav-ham span").forEach(item => {
if (item.classList.contains("is-active")) {
item.classList.remove("is-active");
} else {
item.classList.add("is-active");
}
});
if (document.querySelector(".nav").classList.contains("is-active")) {
this.closeNav();
} else {
anime({
targets: ".close-nav",
duration: 400,
easing: "easeInOutSine",
right: ["-100vw", 0],
opacity: 1,
begin: function() {
document.querySelector(".close-nav").classList.add("is-active");
}
});
anime({
targets: ".nav:not(.is-active)",
duration: 400,
delay: 150,
easing: "easeInOutSine",
right: ["-15rem", 0],
begin: function() {
document.querySelector(".nav").classList.add("is-active");
}
});
anime({
targets: ".nav__background:not(.is-active)",
duration: 400,
easing: "easeInOutSine",
// right: ['-100vw',"-20vw"],
right: ["-18rem", "0rem"],
begin: function() {
document.querySelectorAll(".nav__background").forEach(function(item) {
item.classList.add("is-active");
});
}
});
}
}