Meet the chameleon menu

Moisés Neto
Goiabada
Published in
8 min readFeb 4, 2019

In June 2017 we published our new company website here at Guava. It was quite a milestone for us because this new version consolidated our visual identity.

There were a lot of changes compared to the previous version: we added beautiful new illustrations made by our incredible designers, reviewed our color palette, wrote a lot of new content, implemented a magazine-ish experience with covers on all pages and divided the inner sections with gorgeous curvy shapes.

All these things are amazing, but one important detail to notice is that, from a front-end implementation point of view, it’s very tricky to deal with the interaction between those curvy shapes and fixed elements following the user’s scroll. As the proud craftsmen and women we are, getting rid of the fixed elements wasn’t an option we wanted to pursue, which meant we had to come up with a way to make it work like envisioned. This is the challenge that we’re going to discuss in this post.

Motivation

Contrast affecting the readability of a fixed menu when changing from a section to another

From the early drafts of the new website concept, we had our minds set on the idea of a fixed navigation menu for desktop screens. Locking down its position was going to work very well with our vision of the ideal navigation for this particular project, but it created the issue of maintaining its readability across several different background colors and their different contrast palettes.

First, we considered protecting the menu contents with a flat, simple background color. This solution would address the readability problem pretty well, but — besides looking a little weird in our experiments — it ended up hiding parts of key illustrations in some page sections. We didn’t want that. And there was still a key issue to address…

Enter curved section separators. Solid background fills were getting worse by the minute. The only acceptable course of action left was to find a way to make the menu adjust its colors depending on which section it is hovering, while still respecting all the curvy and non-curvy section boundaries along the way. Damn.

The technique

Searching for existent solutions to our problem, we stumbled upon an exciting library called Midnight.js. By allowing you to change your website header on the fly, this small jQuery plugin caught our attention. Unfortunately for us, the lib only handled rectilinear sections, at least at that point in time.

Even if we couldn’t use the lib, the idea behind its implementation would be the key to developing our own technique.

The behavior we’re aiming for. It can be checked out here.

The menu-changing behavior we wanted was quite peculiar. The menu styles should change immediately when hovering over a different section with different background colors. Also, this hovering could be partial, meaning that just a word, or even a small section of a word, would have different styles. This requirement alone discarded any potential solutions using the classic CSS/JS class toggle technique.

That’s where the Midnight.js implementation comes into play, bringing in the notion of mask elements, that along with menu animations make up the two core concepts applied in our solution.

Mask elements

Initially, when we introduced you to our little challenge, we may have been tempted to speculate a solution in which the menu kinda “knows” the section it is hovering and what styles to apply on itself. I know I have. With the mask elements, the problem is turned exactly the opposite way: the sections are the “intelligent” elements here, knowing what menu or styles to render.

It makes sense, right? Each actual page section has its own fixed menu, hiding the overflows, so the menus don’t overlap. Well, easier said than done.

The first problem that we should notice is that HTML elements with fixed positioning don’t respect positioned parent’s boundaries. Instead, fixed positioning pins an element in place relative to the browser viewport itself. So that clever overflow: hidden; property applied to the section elements won’t work.

Second, we don’t want to slip in the entire menu markup inside each page section. It would be very confusing to mix the navigation with contents of every section on the page; not to mention the accessibility issues. This is where the mask elements come in handy.

These masks are nothing more than transparent elements, created with the exclusive purpose of mimicking the original section format, restraining each section’s menu inside it. Their properties are cloned from the actual section elements. The snippets below illustrate this step of the implementation.

All snippets in this post are generic HTML/jQuery code.

Suppose we have a section element that needs a mask.

For that, we need a mask element on the page (at the end of the page, preferably, for reasons already mentioned).

This mask has some interesting properties:

  • class: the additional classes are used to set some particular properties of a given mask. Remember the curves mentioned above? Well, we need to implement these section curves in CSS and replicate them in the masks. This way, the mask element has the same form of the section element.
  • data-menu-class: this is the class applied to the menu element appended to this mask children.
  • data-menu-mask: a string indicating a selector for its respective section element.

With that out of the way, we can use jQuery to clone the section CSS properties using the attributes defined in the snippet above.

Very straightforwardly we clone the section styles into the mask elements. In addition to the properties above, we need to make the mask absolute-positioned so that they can be precisely above the sections. We can do that using the mask’s additional classes.

After that, our next task should be appending a menu clone to the mask’s children. Let’s not forget the menu modifier class, specified in the data-menu-class attribute. It defines the styles for that section’s menu.

A quick note about accessibility on this approach: the menus are still being replicated. It is recommended to place an aria-hidden="true" property on all menus, except the first.

"Authors MAY, with caution, use aria-hidden to hide visibly rendered content from assistive technologies only if the act of hiding this content is intended to improve the experience for users of assistive technologies by removing redundant or extraneous content."

(W3C WAI-ARIA. Thanks to Thibaut Allender for noticing.)

Since the pure CSS fixed positioning won’t work for the menus, they need to be positioned absolutely, and we need to move them up and down using JavaScript, simulating the fixed-positioning behavior. That’s our hook for the next subsection: menu animation.

3D representation of the sections (yellow), masks (translucid gray) and menu elements

Menu animation

A quick recap: at this point, we have a mask element for each section of our page. These masks are positioned exactly above the real sections, and each one of them contains a clone of the page’s menu absolutely positioned, one on top of the other. Since each mask has the overflow: hidden; property, only the menu for the first section is visible. Nothing should happen on scroll (yet), because of their positioning.

The next step, in the most simplistic approach, should be to update each menu’s top position on page scroll via window.pageYOffset property. This way, they would follow the page scroll, acting as a fixed element. In some simpler scenarios, this should work fine. The thing is that, generally, we’ll be dealing with 5+ menus, updating each one of them on every tiny scroll movement. This approach causes a considerable stutter in movement.

To achieve that elegant smoothness, I’d recommend using JavaScript requestAnimationFrame API.

“The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint."

(MDN web docs: window.requestAnimationFrame)

An applied example of all the mask’s logic and window.requestAnimationFrame use can be checked in the following simplified proof-of-concept hosted in this Codepen.

Scroll and see for yourself!

GPU optimizations

When we were implementing this menu for the Guava website, one of the requirements was to contain these masks elements inside a viewport div. The idea was to restrain all these mask regions to the menu area, but there is one implementation cost that needs to be taken into consideration.

In this case, the viewport would have a fixed position. By doing that, we’ll have to manually move the masks inside it, along with its respective menu clones, on scroll.

Sometimes, the sections masks can assume enormous forms, especially when mimicking a circle or whatever other forms you’re using. Moving these huge elements up and down inside the viewport will very likely hurt the animation’s performance since the browser moves them on every repaint inside the loop.

To work around this problem, we used GPU accelerated CSS transitions. The technique’s details are out of this article’s scope, but there are several good articles about it. You can check them out in the links below:

CSS GPU Animation: Doing It Right — Smashing Magazine

Improving HTML5 app performance with GPU accelerated CSS transitions | Urban Insight

Wrapping up

Took a long time for us to talk about our website menu. Better late than never, right?

Now felt like an excellent opportunity to write about it. A brand new Guava website is cooking, and we’ll probably drop this implementation in the upcoming version due to new design concepts being brought to life. However, in these couple of years, we didn’t see any other web pages implementing this technique which, despite being relatively simple, brings quite some charm to fixed menus. Plus, although we specifically targeted the menu in this post and on our website, this technique can of course be extended to any type of element.

Have you seen other implementations of this effect? Do you know other ways to see it through? Do you have any questions or suggestions? Please let me know in the comments section below. :)

I hope you enjoyed the reading. See you in the next posts!

--

--