T.J. Schiffer
Shapeways Tech
Published in
5 min readApr 18, 2017

--

Marvelous Mobile Menu in CSS

As part of our site-wide navigation design I made a conscious decision to use transitions for any instance where a user triggers a change on the page. For most elements this would be a simple fade-in/fade-out or slide in. But for our user widget menu I had a vision: the round user avatar would expand out and reveal a full screen menu.

In front end development at Shapeways, we are typically given static renders that indicate what the page should look like at a specific resolution or state. This process leaves us with the initial and final state of what a web page will look like. It’s our job to bring those renders to life, creating responsive pages with rich user interactions and transitions. I typically design my transitions using a basic philosophy: if I have an initial state and final state, what’s the most reasonable and visually appealing way to get from one to the other?

The main trick of this animation is that the full screen menu appears to be stationary, when in fact the menu is animating its position relative to the expanding circle wrapper div. As the circle expands the menu inside is transitioning its top and left attributes to cancel out its containing block’s position. This results in an effective position of zero for top and left relative to the viewport. By leveraging viewport-percentage widths we can make sure the content has a height and width that perfectly fills the page without having to set the height and width relative to the fixed containing block. Let’s get right into the CodePen:

Much of the css is styling for text or static elements so let’s go over the transition specific details, which I have added in plain CSS:

.user-account-widget__avtr {
border-radius: 50%;
height: 30px;
width: 30px;
overflow: hidden;
position: absolute;
z-index: 0;
transition: z-index 0s .4s;
}
.user-account-widget__cntnt-wrrpr {
background: #fff;
border-radius: 50%;
-moz-box-shadow: 0 4px 8px 0px rgba(68,82,88,0.2);
-webkit-box-shadow: 0 4px 8px 0px rgba(68,82,88,0.2);
box-shadow: 0 4px 8px 0px rgba(68,82,88,0.2);
left: 89.8vw;
overflow: hidden;
position: fixed;
height: 0;
top: 30px;
width: 0;
z-index: 999;
-moz-transition: height .4s ease-in-out, left .4s ease-in-out,top .4s ease-in-out, width .4s ease-in-out;
-o-transition: height .4s ease-in-out, left .4s ease-in-out,top .4s ease-in-out, width .4s ease-in-out;
-webkit-transition: height .4s ease-in-out, left .4s ease-in-out, top .4s ease-in-out, width .4s ease-in-out;
transition: height .4s ease-in-out, left .4s ease-in-out, top .4s ease-in-out, width .4s ease-in-out;
}

The content wrapper is transitioned from a size of zero and is positioned under the the center of the avatar. Top, left, height, and width must all be transitioned for the content wrapper. transition: all could be used but I prefer to be explicit in order to avoid a situation where someone makes changes to the final state without explicitly indicating it should be transitioned.

Unfortunately the fixed position content wrapper must be positioned manually at this position relative to the viewport. While it may be possible to use absolute positioning to get the content wrapper under the avatar, it is important that the opened menu will have an effective zero top and left position relative to the viewport. Ideally we would have the content wrapper be absolutely positioned relative to the user widget with the menu fixed positioned inside of the wrapper. This is not possible due to the fact that fixed positioned element’s containing block is the viewport and as such overflow: hidden on the containing block will not apply.

In this example case, the avatar’s horizontal position is set in percentage and its vertical position in pixels and we position the content wrapper with matching units directly under the center of the avatar. A box shadow is added for effect on the circular content wrapper but could be replaced with a border if you so prefer.

.user-account-widget__cntnt {
height: 100vh;
left: -89.8vw;
position: absolute;
top: -30px;
width: 100vw;
-moz-transition: left .4s ease-in-out, top .4s ease-in-out;
-o-transition: left .4s ease-in-out, top .4s ease-in-out;
-webkit-transition: left .4s ease-in-out, top .4s ease-in-out;
transition: left .4s ease-in-out, top .4s ease-in-out;
}

The content will reverse this position so that it replicates a left:0; top:0; fixed positioning. Set this menu to be viewport 100% height and width to ensure full screen coverage. Only left and top will be transitioned while the wrapper content is expanding.

.user-account-widget__open {
position: fixed;
height: 100%;
width: 100%;
}

The click event is used to add a class to the body that triggers the menu to open. While we could add the class to the content wrapper, adding this class to the body allows the entire content to be fixed. This avoids an issue where the content could be scrolled under the menu instead of scrolling the menu itself.

.user-account-widget__open .user-account-widget__avtr {
z-index: 9999;
transition: none;
}

It’s worth noting that you have to do some z-index trickery on the avatar itself since you want the avatar to appear above the open menu as soon as it opens but then wait until the closing transition is complete before returning to it’s closed z-index value. The transition is set on the non-opened avatar then overridden on menu open.

.user-account-widget__open .user-account-widget__cntnt-wrrpr {
height: 300vh;
left: calc(-150vh + 89.8vw);
top: calc(-150vh — 30px);
width: 300vh;
}
.user-account-widget__open .user-account-widget__cntnt {
left: calc(150vh — 89.8vw);
top: calc(150vh + 30px);
}

This wrapper will expand in a perfect circle to a height and width equal to the diameter of the circle — most of which will be off screen in order to make sure the entire screen will fall within the fixed circle. At the same time, the position of this circle will be such that the center of the circle will always be directly under the center of the avatar. The content’s position (relative to the circular wrapper) will be the opposite of its container in order maintain the effective left: 0; top: 0 position relative to the viewport. A radius of 150vh (150% of the view height) ensures coverage for screens in portrait.

@media (all) and (orientation: landscape)
.user-account-widget__open .user-account-widget__cntnt {
left: calc(140vw — 89.8vw);
top: calc(140vw + 30px);
}
}

For landscape screens, we can use a media query to set the radius to 150vw which will give us coverage for screens in landscape mode.

What we end up with is a menu that explodes out of a circular avatar that is based entirely in CSS transitions. Thanks for reading!

--

--