Let’s go, gamers: how we made the GAMINGbible menu

Joey Imlay
LADbibleGroup
Published in
7 min readNov 20, 2020
A PS4 controller next to a laptop with code on the screen.
Let’s go, gamers. Photo by Hello Lightbulb on Unsplash

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.

gamingbible.co.uk, as seen on a desktop screen
gamingbible.co.uk as seen on desktop

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.

gamingbible.co.uk, as seen on a mobile device, with the menu closed
gamingbible.co.uk as seen on a mobile device, with the menu closed
gamingbible.co.uk, on a mobile device, with the menu open
gamingbible.co.uk as seen on a mobile device, with the menu open

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.

two Javascript functions showing the use of classList.add and classList.remove
A single CSS class toggles the open state of the menu.

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:

CSS code to hide scrollbars
Cross-browser CSS to hide scrollbars in the menu.

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.

A useEffect hook containing an IntersectionObserver.
The IntersectionObserver loops over the menu icons, checking their isIntersecting state.

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:

A useEffect hook that connects and disconnects observers as needed
On re-render, observers are reattached as needed, and unused observers are removed.

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.

A Javascript function that removes the observer, triggers a scroll, then reattaches the observer
Detach the observer, calculate the scroll distance, scroll, and reattach.

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.

A useEffect hook that adds event listeners to click and blur events
The menu closes on click and blur events.

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:

A Javascript function to handle scroll events
If the menu is scrolled more than 20 pixels over its height in either direction, the menu closes.

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.

--

--

Joey Imlay
LADbibleGroup

Software engineer at Accenture Next Gen Engineering. She/her. DFTBA.