A Priority+ Navigation With Scrolling and Dropdowns

The following is a guest post by Micah Miller-Eshleman. Micah designed a variation of the "Priority+ Navigation" concept and uses it in production at the college he works for. I always dig a show & tell behind the thinking and creation of a design pattern, especially when it's working out there in the real world.

Exposing long navigation menus on small screens is tricky. Hamburger menus are everywhere, although often discouraged. Displaying "just enough" navigation at every breakpoint can feel like an impossible task. This is especially true for template developers needing to accommodate an arbitrary number of menu items.

The Priority+ design pattern seeks to display as many items as possible given an arbitrary screen width, while making the rest accessible via a single click. I'll go over the implementation I worked on at Goshen College that includes both dropdown menus and horizontal scrolling, which I've yet to find in the wild:

User scrolling horizontally over navigation menu

1. Horizontal scrolling with sticky item with pure CSS

Let's start off with a simple horizontally scrolling menu:

See the Pen Priority+ nav with a sticky item by Micah Miller-Eshleman (@pranksinatra) on CodePen.

(For the curious, I'm using BEM naming conventions for the classes and PostCSS for some simple conviences like nesting and variables.)

Notice in the above demo how resizing the screen width above/below 480px toggles the stickiness of "GC Admissions", the first menu item. This is achieved using two wrappers: an outer wrapper containing all menu items scrollable at <480px widths and an inner wrapper containing all non-sticky menu items and scrollable at >480px widths.

2. Adding buttons and dropdown menus

Here I've extended the prior demo with dropdown menus and click-to-scroll buttons. I needed some JavaScript to help with the scrolling functionality.

See the Pen Priority+ nav with dropdown menus and scroll buttons by Micah Miller-Eshleman (@pranksinatra) on CodePen.

A quick note on dependencies: I inline a partial classList, performance.now, and requestAnimationFrame polyfill for IE9+. Font Face Observer is used to detect when the Source Sans Pro font has loaded. jQuery helps make dropdown menus tab-navigable by listening to the :focus state of their child links using the focusin() and focusout() bindings. Please load it asynchronously (or find a lighter alternative).

2a. Click-to-scroll buttons

Buttons are important not just to enable horizontal scrolling for mouse-and-keyboard users, but to indicate that scrolling is possible. The concept was taken from the Apple.com iPhone page:

At it's core, a button click scrolls menu items by updating the scrollLeft property of the relevant wrapper. This is then animated with an easing function to simulate smooth "momentum" scrolling.

You'll also notice some logic for showing/hiding buttons, determining which wrapper to scroll, etc. To make these decisions, we listen to the menu's horizontal scroll position and the window's width (with debounced event handlers for performance).

2b. Dropdown menus on horizontal scroller

Building horizontal-scrolling dropdown menus turned out to be the most challenging part of this project.

The menu's basic HTML structure consists of scrollable wrapper containing a list of menu items, some of which contain dropdown menus. Menu items are relatively-positioned and their dropdowns are absolutely-positioned so they scroll as a single unit but do not affect each other's width.

An explicit height must be given to the wrapper so the dropdowns don't get cut off. We then run into the issue of having a really tall wrapper that either pushes down or covers up subsequent content:

See the Pen Positioning scrollable wrappers by Micah Miller-Eshleman (@pranksinatra) on CodePen.

The solution I opted for was keeping the menu collapsed until it's interacted with, at which point we transition immediately to state #3 (and vice versa). This is accomplished by listening for the mouseover and touchstart events on the component and the wrapper.

The demo is purposefully simplified with a single wrapper. For multiple wrappers, I actually toggle the overflow property (hidden/visible) on the .nav component instead of the height of individual wrappers, but it's the same underlying concept.

If you find more elegant ways of doing this, I'm all ears!