Website menu animation using GSAP

in Front-end | June 2, 2020
website menu animation frontend tricks

Let’s consider creating a simple menu animation using GSAP.

That’s the result we’ll get at the end.

Let’s start implementing website menu.

1.HTML

Our structure will be as follows:

<div class="wrapper">
    <div class="base">
      <button class="menu_trigger btnOpenMenu">Menu</button> // Menu opening button
      <h1 class="title">Maketodoro</h1>
      <nav class="menu mainMenu">
        <div class="menu_w_decor"> // The wrapping block where the decors are stored which will be animated http://prntscr.com/q8nlwm
          <div class="menu_decor_w_line">
            <span class="menu_decor_line"></span>
          </div>
          <div class="menu_decor_w_line lg_mod">
            <span class="menu_decor_line"></span>
          </div>
          <div class="menu_decor_w_line lg_mod">
            <span class="menu_decor_line"></span>
          </div>
          <div class="menu_decor_w_line lg_mod">
            <span class="menu_decor_line"></span>
          </div>
          <div class="menu_decor_w_line">
            <span class="menu_decor_line"></span>
          </div>
        </div>
        <button class="menu_trigger close_menu_mod btnCloseMenu">Close</button> // Menu close button
        <ul class="menu_list"> // Menu items
          <li class="menu_list_item">
            <a href="#" class="menu_list_link">Home</a>
          </li>
          <li class="menu_list_item">
            <a href="#" class="menu_list_link">About</a>
          </li>
          <li class="menu_list_item">
            <a href="#" class="menu_list_link">Hover Mee</a>
          </li>
          <li class="menu_list_item">
            <a href="#" class="menu_list_link">Contact</a>
          </li>
        </ul>
      </nav>
    </div>
  </div>

2.SCSS

// Mixin with properties for better animation performance.
@mixin d3() { 
	will-change: transform;
	transform: translateZ(0);
}

html {
	font: 400 10px/1.33 'Montserrat';
}

body {
	background-image: linear-gradient(135deg, #8f24ed 30%, #107cb3 100%);
}

.wrapper {
	display: flex;
	flex-direction: column;

	height: 100vh;
	overflow: hidden;
}

.base {
	position: relative;

	display: flex;
	flex-direction: row;
	max-width: 37.5em;
	width: 100%;
	height: 66.7em;
	margin: auto;
	padding: 2em;

	font-size: 1rem;

	border-radius: 1em;
	box-shadow: 0 0 0 1.2em rgba(0, 0, 0, 0.1), 0 0 1px 1px rgba(0, 0, 0, 0.1), inset 0 0 0 1.2em white;
	background-color: #eaecef;
	overflow: hidden;

	@media screen and (max-width: 374px) {
		font-size: .8rem;
	}

}

// Menu opening button
.menu_trigger {
	position: absolute;
	top: (2.4em/1.6);
	right: (2.6em/1.6);
	z-index: 1;
	
	font-size: 1.6em;
	font-weight: 900;
	letter-spacing: -1px;
	text-transform: uppercase;
	
	cursor: pointer;

	will-change: transform;

	// The closing button initially has such a modifier, it is not visible + there is an offset to the left by 1em
	&.close_menu_mod {
		opacity: 0;
		pointer-events: none;

		transform: translateX(-1em);
	}

	// Styles for the cap which is under the button http://prntscr.com/q8noc3
	&:before {
		position: absolute;
    bottom: -2px;
		left: 0;
		z-index: -1;
		
    width: 100%;
    height: (1.2em/1.6);
		
		background-image: linear-gradient(0deg, #ee43bd 0%, #ffd779 100%);
		
		transform: translateX(-.7rem);
		will-change: transform;
		content: "";
		
    transition: all 0.2s ease;
	}

	// At hover we shift the plate that we have hanging at before to zero 
	&:hover,
	&:focus {
		&:before {
			transform: translateX(0);
		}
	}

}

.title {	
	margin: auto;
	max-width: 100%;

	font-weight: 900;
	font-size: 4em;
	text-align: center;
	line-height: (2.8em/3.2);
	text-transform: uppercase;
}

// The menu will hang in absolute and stretch to the full width and height of the parent (block .base). Initially it will have opacity: 0 and pointer-events: none. We will change these properties later using gsap.

.menu {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 1;

	display: flex;
	flex-direction: column;
	width: 100%;
	height: 100%;
	padding: 2em;

	opacity: 0;
	pointer-events: none;
}

// Center the menu items

.menu_list {
	margin: auto;
	width: 100%;
}

.menu_list_item {
	margin-bottom: .8em;
}

.menu_list_link {
	display: block;

	font-size: 5em;
	color: #282828;
	font-weight: 900;
	text-decoration: none;
	text-transform: uppercase;
	letter-spacing: -1.2px;

	transition: color .3s ease;

	@include d3();

	&:hover,
	&:focus {
		color: #8e24ec;
	}

}

// As mentioned above, .menu_w_decor is a wrapper for all decors and is stretched to the full width and height of the menu.
.menu_w_decor {
	position: absolute;
	top: 0;
	left: 0;

	display: flex;	
	flex-direction: row;
	width: 100%;
	height: 100%;

	border-radius: 1em;
	overflow: hidden;
}

// .menu_decor_w_line is a wrapper for the menu_decor_line block, the width of the first and the last element is 2.1em. For all the others we calculate the width with calc(). this way we get this picture http://prntscr.com/q8nr2o. This block also has overflow: hidden, because we need the internal content of this block to stay within the size of the parent block when offset in the x axis.

.menu_decor_w_line {
	height: 100%;
	position: relative;
	
	overflow: hidden;

	&.lg_mod {
		width: calc(33.33% - 1.4em);
	}

	&:first-child {
		width: 2.1em;
	}

	&:last-child {
		width: 2.1em;
	}

}

// Here are the styles directly for the strips themselves. The stripes end up looking like this http://prntscr.com/q9opno. I specifically removed all the other stripes to make it clearer.

.menu_decor_line {
	position: absolute;
	top: 0;
	left: 0;

	width: 100%;
	height: 100%;
	opacity: 0;

	background-color: #fff;

	@include d3();

	&:before {
		position: absolute;
		top: 0;
		left: 0;

		width: 1px;
		height: 100%;

		background-color: #edeff5;

		content: "";
	}

}

3.JavaScript

Let’s declare the variables.

let $btnOpenMenu,
 $mainMenu,
 $mainMenuItem,
 $btnCloseMenu,
 $menuDecorLine,
 menuTl;

At onReady we assign values to them.

$(document).ready(function ($) {
	$btnOpenMenu = $('.btnOpenMenu');
	$btnCloseMenu = $('.btnCloseMenu');
	$mainMenu = $('.mainMenu');
	$mainMenuTitle = $('.menu_list_link');
	$menuDecorLine = $('.menu_decor_line');
	menuTl = gsap.timeline({paused: true});
});

Create a function with animation and time line.

const menuAnimFunc = () => {
	menuTl
};

We show the menu.

.set($mainMenu, { 
	opacity: 1,
	pointerEvents: "auto"
})

We set the initial rank for the bands -100%. Thus, they will not be visible from the start, because the parent has an overflow: hidden, so we get the effect of curtains.

.fromTo($menuDecorLine, 0.25, { 
	xPercent: -100,
}, {
	xPercent: 0,
	opacity: 1,
	ease: Power1.easeOut
})

Animate menu items one by one using staggerFromTo.

.staggerFromTo($mainMenuTitle, .3, {
	x: -50,
	opacity: 0,
	ease: Back.easeOut.config(1)
}, {
	x: 0,
	opacity: 1
}, .2)

Animate the appearance of the close button.

.to($btnCloseMenu, .1, {
	opacity: 1,
	pointerEvents: "auto",
	translateX: 0
})

Add onClick event to the open menu button. We make a check if menuTl.isActive() returns false, i.e. if the animation does not work at the moment, then we start it.

$btnOpenMenu.on('click', () => {
	if(!menuTl.isActive()) {
		menuTl.play();
	}
});

Exactly the same event we hang up on the menu close button. Only here we make menuTl.reverse(); instead of menuTl.play();

$btnCloseMenu.on('click', () => {
	if(!menuTl.isActive()) {
		menuTl.reverse();
	}
});

At onLoad we start the function with animation.

$(window).on('load', function () {
	menuAnimFunc();
});

You can see the final result by clicking on the link. That’s all, thank you for your attention.