Let’s go, gamers: how we made the GAMINGbible menu
Written by Kieran Duff and Joey Imlay.
In autumn 2020 we built GAMINGbible, the fifth site in the LADbible Group family. The designs were ambitious and introduced many new elements to our React design system, chief among which was the navigation menu, an element unlike anything we’d built before.
On desktop, the menu is fairly straightforward. In the left-hand column of each of our primary pages lives a column of icons with corresponding labels. Tap on one to navigate to that page, where the icon changes size and colour, and the label moves to the inside of the icon.
On mobile devices, however, the menu changes its behaviour entirely. A single icon floats at the bottom of the screen indicating the page currently in view. On tapping this icon, the rest of the menu reveals itself, as icons slide into view from behind the current icon. Scrolling up and down the menu causes each icon to snap into position as scrolling ends, and tapping an icon triggers both navigation and the menu to close, with the tapped icon becoming the new active icon. Scrolling to one particular icon triggers a submenu to animate horizontally into view.
Thanks to React, both of these menus are powered by a single component. Achieving the mobile-only animations was the result of a number of different web technologies coming together, and in some cases having to create our own versions of existing functionalities to better suit our needs. Here, we’ll explain the technical choices we made whilst building this component — rather than taking you through our process step by step, we’ll show you the cool bits of tech we used to create this component.
Opening the menu
To make opening and closing the menu as lightweight as possible, we made this hang off a single CSS class, is-open
. The menu can only open through tapping the icon in the bottom left corner of the screen, but there are several ways to close it, so the easier to control the open state, the better.
The HTML of the menu is an unordered list with six menu items, each containing an icon sub-component. When closed, the menu has a fixed height equal to one menu item. When it’s open, the height is equal to the height of all six elements plus a margin-top
on the first item equal to the combined height of the list items. This margin-top
allows us to scroll all the items apart from the last one out of view, giving us the scrolling effect even though all content is in view.
To add to the illusion of a floating menu, we hid the scroll bars with the following CSS:
Scrolling the menu
One of the main animation features of the menu is that the icon that’s currently in the bottom-left position — where the active icon appears when the menu is closed — gets slightly bigger than the surrounding icons. To achieve this, we used the Intersection Observer API, which offers an easy way to watch for changes in the intersection of an element with one of its ancestors. Here, we use an intersection observer to watch for the icon scrolling in and out of view, and react to this by toggling a CSS class, is-active,
on the currently active icon, which in turn triggers some CSS styling changes.
Take a look at our useEffect
hook below. When the menu component mounts, we create an intersection observer with a function which loops over the entries — our menu icons — and looks at the isIntersecting
property on each. If this value is true
, we know this item has just moved into view, and we can add the is-active
class to this icon. Similarly when that value is false
, that tells us that the icon has moved out of view, and that we can remove the is-active
class. We set the root as our navRef
reference, attaching it to the parent <ul>
element, as this is the element the user is scrolling. This intersection observer is assigned to an observer reference we created at the top of the component. Here’s a simplified example of that.
We could then start observing with the observe()
method, but there are some considerations we need to make when using intersection observers in React. When React updates the element, the fresh instance of the element it creates in the DOM is no longer being observed. Our fix is another useEffect
hook which watches for re-renders triggered by React, reattaches the observers as needed, and returns a function which removes the old ones, like so:
These two things combined allow us to toggle CSS classes as icons scroll in and out of view, and to style them up accordingly — with the added bonus we get to use a cool piece of tech. We think it’s an interesting use case of the Intersection Observer API, and we hope you do too!
Snapping icons into place
As we’ve mentioned, active icons need to look different from the other icons in the menu — in our case, the key difference is that the active icon is bigger than the others. However, this dynamic sizing of each icon meant that we couldn’t use standard CSS scroll-snap
functionality, as snap points would have to be equally dynamic. So we created our own solution, using a combination of React refs and scrollTo()
functionality.
We listen for scroll events on the menu, debouncing this event by 300ms. When the user stops scrolling for 300ms or more, we call moveTo()
with the icon that our intersection observer has marked as active. This then centres the active icon in the active area, in the bottom left of the screen.
The final piece of animation is the horizontal submenu that slides into view when the Platforms icon is active. Tapping the Platforms icon will by default navigate the user to the /playstation
route, but we need to provide access to our other platform-specific pages too. Our intersection observer watches for the Platforms icon to snap into the active position, then adds a CSS class to the submenu which slides it into view from the left. The submenu is itself horizontally scrollable, with our platform pages listed inside. Tapping any of these options will trigger the expected closing of the menu as the browser navigates to the relevant page.
Closing the menu
There are two actions that trigger the closeMenu
function. The first is tapping anywhere on the screen other than the menu. This is simple enough to handle with a click event listener on the window and in the callback we close the menu. However, many of our article pages utilise iframes to display content such as social embeds and videos. The click event wouldn’t work when tapping on these iframes as it would cause the event listener to fire inside the iframe instead of our current document, and to therefore have no effect. To handle this scenario we listen for the blur event on the window and check that the active element when blurred has the tagName of IFRAME, if so we then close the menu.
Finally we put both of these event listeners into our component mount use effect so as to only attach them once.
The second trigger to close the menu is a scroll event. This can be achieved using a simple window scroll event listener but we wanted to implement a buffer to avoid any jumping around or side effects from our various animations. When the user scrolls any further than 20 pixels on the page in either direction, it will close the menu. The callback for the scroll listener looks like this:
Our openMenu()
function adds the event listener with handleScroll
to the window; closeMenu()
removes it.
So that’s how we built the GAMINGbible menu. We’re glad to have had this chance to explore the potential of current web technology, and we’re equally glad to be able to show you what’s possible. Let us know if our work here has inspired you to try out any of the functionality we’ve mentioned.