Animated Border Menus

A tutorial on how to create a off-canvas icon navigation with an animated border effect. The menu effect is inspired by CreativeDash’s bounce menu for mobile apps.

The other day I saw a really nice concept of a menu on the UI8 site. CreativeDash implemented that gorgeous concept and I instantly had some ideas for more effects involving border transitions but also with the desktop in mind. So today I want to show you how to create something like that and provide some more inspirational examples.

In this tutorial we will be going through demo 2 where the menu icon is in the top left corner and the border is thickest on the left side.

Please note that we’ll be using transitions and animation on pseudo-elements which won’t work in some browsers (e.g. Safari and Mobile Safari).

So, let’s get started!

The Markup

The HTML structure for our menu will consist of a nav element that will contain a trigger anchor and an unordered list with the menu items which will consist of icons:

<nav id="bt-menu" class="bt-menu">
	<a href="#" class="bt-menu-trigger"><span>Menu</span></a>
		<li><a href="#" class="bt-icon icon-zoom">Zoom</a></li>
		<li><a href="#" class="bt-icon icon-refresh">Refresh</a></li>
		<li><a href="#" class="bt-icon icon-lock">Lock</a></li>
		<li><a href="#" class="bt-icon icon-speaker">Sound</a></li>
		<li><a href="#" class="bt-icon icon-star">Favorite</a></li>

Let’s style this.



Note that the CSS will not contain any vendor prefixes, but you will find them in the files.
Let’s use the border-box box-sizing:

*::before {
	box-sizing: border-box;

And let’s set some styles for the body and the main container:

body  {
	background: #04a466;

.container {
	padding: 80px;

The padding will help providing some space around our content so that when the border appears, we guarantee that there is enough space around.

The main menu element will have position fixed so that, no matter where we are in the page, the border is always around the viewport. We set an initial border style which we will transition to a bigger border. Setting the initial height to 0 will make sure that the menu does not cover anything initially. The “backward” or closing height transition will have a delay of 0.3s:

.bt-menu {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 0;
	border-width: 0px;
	border-style: solid;
	border-color: #333;
	background-color: rgba(0,0,0,0);
	transition: border-width 0.3s, background-color 0.3s, height 0s 0.3s;

When we open the menu, we’ll set the height to 100% (but we won’t transition that property) and the border will animate to 90px on the left side and 30px on all the other sides. The background color will be semi-transparent using an RGBA value. This will server as out overlay color: {
	height: 100%;
	border-width: 30px 30px 30px 90px;
	background-color: rgba(0,0,0,0.3);
	transition: border-width 0.3s, background-color 0.3s;

Now we have to use a little trick. We will add another element using JavaScript which will server as a dummy container covering the whole page except the border. This will allow us to distinguish where we are clicking in order to close the whole thing. We don’t want the menu to close when clicking on the border but only when clicking in the space between.

.bt-overlay {
	position: absolute;
	width: 100%;

When we open the menu, this element will have full height:

.bt-menu-open .bt-overlay {
	height: 100%;

Let’s style that little trigger element. We’ll give it a fixed position and we’ll show it in the top left corner of the page:

.bt-menu-trigger {
	position: fixed;
	top: 15px;
	left: 20px;
	display: block;
	width: 50px;
	height: 50px;
	cursor: pointer;

The trigger anchor itself will serve as a container and the span will be the middle line of our hamburger menu icon. So we position it in the middle by setting the top to 50% and giving it a negative top margin of half of its height:

.bt-menu-trigger span {
	position: absolute;
	top: 50%;
	left: 0;
	display: block;
	width: 100%;
	height: 4px;
	margin-top: -2px;
	background-color: #fff;
	font-size: 0px;
	user-select: none;
	transition: background-color 0.3s;

When opening the menu, we will make a cross out of the icon. The other two lines will be created by pseudo-elements and when the menu is open, the middle line will disappear:

.bt-menu-open .bt-menu-trigger span {
	background-color: transparent;

Now, let’s create the two other lines. The pseudo-elements will be positioned absolutely and their height is going to be the same like of their parent by setting it to 100%:

.bt-menu-trigger span:before,
.bt-menu-trigger span:after {
	position: absolute;
	left: 0;
	width: 100%;
	height: 100%;
	background: #fff;
	content: '';
	transition: transform 0.3s;

For positioning them correctly, we’ll use translateY:

.bt-menu-trigger span:before {
	transform: translateY(-250%);

.bt-menu-trigger span:after {
	transform: translateY(250%);

The cross will be formed when opening the menu by setting the translateY to 0 and rotating the pseudo-elements accordingly:

.bt-menu-open .bt-menu-trigger span:before {
	transform: translateY(0) rotate(45deg);

.bt-menu-open .bt-menu-trigger span:after {
	transform: translateY(0) rotate(-45deg);

The unordered list with our icons will also have a fixed position and we’ll set it to the left side of the window:

.bt-menu ul {
	position: fixed;
	top: 75px;
	left: 0;
	margin: 0;
	padding: 0;
	width: 90px;
	list-style: none;
	backface-visibility: hidden;

Let’s set the list items and the anchors to display: block and give them full width:

.bt-menu ul li,
.bt-menu ul li a {
	display: block;
	width: 100%;
	text-align: center;

Each list item will be hidden initially and the opacity will be 0. The “backward” transition of the visibility will be delayed until all the other transitions of the transform and the opacity are finished:

.bt-menu ul li {
	padding: 16px 0;
	opacity: 0;
	visibility: hidden;
	transition: transform 0.3s, opacity 0.2s, visibility 0s 0.3s;

Now we will transform each of the list items differently so that they are all placed in the middle and to the left until they are hidden (-100% on the Y axis):

.bt-menu ul li:first-child { 
	transform: translate3d(-100%,200%,0);

.bt-menu ul li:nth-child(2) { 
	transform: translate3d(-100%,100%,0);

.bt-menu ul li:nth-child(3) { 
	transform: translate3d(-100%,0,0);

.bt-menu ul li:nth-child(4) { 
	transform: translate3d(-100%,-100%,0);

.bt-menu ul li:nth-child(5) { 
	transform: translate3d(-100%,-200%,0);

When opening the menu, the list items will become visible (instantly, because we are not setting a transition for it) and they will fade in. They will also move to their original positions by setting the transform3d to 0 for all axes: ul li {
	visibility: visible;
	opacity: 1;
	transition: transform 0.3s, opacity 0.3s;
	transform: translate3d(0,0,0);

Now, let’s style the anchors. We will use an icon font and include the font reference and the icon classes in another CSS which will be provided by a service like Fontastic or the IcoMoon app.

By setting the font size of the anchor to 0 and make it transparent, we’ll hide the text:

.bt-menu ul li a {
	display: block;
	outline: none;
	color: transparent;
	text-decoration: none;
	font-size: 0px;

We’ll reset the font size for the pseudo-element which contains the icon. We’ll need to use a pixel-based value because the main element has a font-size of 0 so ems won’t work here:

.bt-menu ul li a:before {
	color: #04a466;
	font-size: 48px;
	transition: color 0.2s;

On hover we’ll make them white:

.bt-menu ul li a:hover:before,
.bt-menu ul li a:focus:before  {
	color: #fff;

And last, but not least, we want the icons to be smaller on mobile screens:

@media screen and (max-height: 31.125em) {
	.bt-menu ul li a:before {
		font-size: 32px;

And that’s all the style. Now, let’s move on to the JavaScript.

The JavaScript

Our script is pretty straightforward; when we click on the trigger anchor, we toggle the class bt-menu-open and bt-menu-close on the nav element. (Adding the closing class is only needed if you are using animation for the trigger icon effect, just like we do in demo 1. This will allow us to only play the backward animation, when we close the menu).

When we click on the overlay, we will close the menu. We’ll also add some touch support:

(function() {

	function mobilecheck() {
		var check = false;
		(function(a){if(/(android|ipad|playbook|silk|bbd+|meego).+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
		return check;

	function init() {

		var menu = document.getElementById( 'bt-menu' ),
			trigger = menu.querySelector( '' ),
			// event type (if mobile, use touch events)
			eventtype = mobilecheck() ? 'touchstart' : 'click',
			resetMenu = function() {
				classie.remove( menu, 'bt-menu-open' );
				classie.add( menu, 'bt-menu-close' );
			closeClickFn = function( ev ) {
				overlay.removeEventListener( eventtype, closeClickFn );

		var overlay = document.createElement('div');
		overlay.className = 'bt-overlay';
		menu.appendChild( overlay );

		trigger.addEventListener( eventtype, function( ev ) {
			if( classie.has( menu, 'bt-menu-open' ) ) {
			else {
				classie.remove( menu, 'bt-menu-close' );
				classie.add( menu, 'bt-menu-open' );
				overlay.addEventListener( eventtype, closeClickFn );




And that’s it! I hope you enjoyed this tutorial and find it useful!
Make sure to check out the other demos. The last one is a concept for a fullscreen video player.

Tagged with:

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay in the loop: Get your dose of frontend twice a week

πŸ‘Ύ Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!

Feedback 108

Comments are closed.
  1. Hi Mary Lou, great work!

    Unfortunately your Restaurant Website and Gallery Template has an error on the map (Firefox). I think that error is recent. If you can fix that, I’ll be much appreciated. Sorry write here but the other comments are closed. Thanks.

  2. very niice !
    how could i have reference to change the icone
    any icon base for the code of like {content: “\e002”}
    thanks a lot

  3. Mary, can you write an article on how you come up with these ideas or what inspires them? Some of this stuff I look at and wonder how you come up with some of these ideas. I’m sure others would agree and read it.

    • I was just reading through all the other comments. You’re building quite a fan club! (Deservedly so)

  4. Nice in the browser, but the on Galaxy Note the responsive Ads block the demos with bottom buttons.

  5. Thahank you for this is a great tutorial.. I’ll use it on my next project!

  6. Wonderful !

    You made a small error in THE JAVASCRIPT part :
    var overlay = document.createElement('div'); overlay.className = 'bt-overlay'; menu.appendChild( wrapper );

    ‘wrapper’ must be replaced by ‘overlay’ in the last line.

  7. Thanks a lot for the tutorial . I just had one question for links in the same page i mean sections with id’s . Can you tell me how can i close the menu automatically as i click the link as the section scrolls but the menu stays open .

    • Anyways no reply for my question . Anyone having problems with menu icon moving on scrolling in Chrome mobile ?? Other browsers its perfect .

      A quick tip to fix a box when you tap the icon

      Add this to


      -webkit-tap-highlight-color: rgba(0,0,0,0);
      -webkit-tap-highlight-color: transparent; /* For some Androids */

    • hey nik…i have the same problem….it works on other browsers but chrome mobile….so i was wonder u said add
      -webkit-tap-highlight-color: rgba(0,0,0,0);
      -webkit-tap-highlight-color: transparent; /* For some Androids */ to the .btn-menu-trigger

      but it still does not function …any idea on that??

  8. Unfortunately, it didn’t work for me. All I get is the ‘bottom bun of the hamburger’ in chrome (all three layers in firefox), but nothing works.

    • So weird Joel, it works fine here on all modern browsers. Double check on the javascript , it might be turned of or something !

  9. I’m somewhat new to web development but when ever I attempt to click on the + nothing happens. The border doesn’t pull in. Any help would be much appreciated.

  10. I loved that, you always inspire me, Great demos <3.

    I just have a question please, Why you are using pure javascript to create elements and to deal with ? why not jQuery ? is there a particular reason for that ?

    Thank you.

  11. Can someone provide some no conflict tops on this script – seem to be having issues working it with other instances on the page.



    • Hi there, I downloaded a copy from GitHub, but can you tell me what is the difference between yours and Mary Lou’s examples? I couldn’t find a difference while viewing.. (maybe its something inside, but how does the inside makes it different than Mary Lou’s?)

  12. very good job this is fantastic.
    I request your permission to use this code and put it on my blog by some changes.

  13. Hey,
    I really like these effects. Would it be possible to edit the sixth effect so it has actual video in it. This way we can see how it would actually work.

  14. Waw ! Un grand bravo pour ce super menu, j’adore !
    Personnellement, j’ai rΓ©ussi Γ  l’implanter sur un WordPress mais j’ai dΓ» abuser de jQuery pour ajouter les Γ©lΓ©ments manquants au menu de base.
    Encore un grand bravo πŸ™‚

    Congratulation for your work. Beautiful menu, i like it ! I use this menu on WordPress so jQuery has help me πŸ™‚

  15. I’d like to know if there is any way to close the menu automatically when I click on a menu option…

    Anyway, great work!

    Thank you