Jan Miksovsky’s BlogArchive2018 AboutRSSJSONContact

Building a great menu component is so much trickier than you'd think

We’ve released v2.2 of the Elix web components library, which includes some new components for menus:

We want all these menu components to feel as polished and natural as native OS menus. Native menus have a number of subtle details, and getting the UI details right turns out to be outrageously complex. Menus are a good example of the fractal nature of UI design.

Just to get started, we need to be able to position a menu with respect to a source button.

Because these positioning rules generally apply to all popups invoked from buttons — not just menus, but also things like combo boxes — we’ve enshrined responsibility for position popups relative to a source button in a general-purpose PopupSource class.

Two ways of selecting a menu command with a mouse

Most people have probably never noticed there are two different ways of using a mouse to select an item from an OS menu:

Nearly every web menu handles only the first method: selecting a menu item in two clicks. But both macOS and Windows support selecting menu items in a drag operation, which can feel faster and more responsive. If you’re reading this on a laptop, try using your mouse/trackpad now to select a browser menu command using both approaches. Observe the different feel of the two approaches. Which do you normally use?

(I seem to recall that the original Mac OS supported only menu selection with a drag, while Windows supported both methods. Windows generally had better keyboard support for menu navigation, and once Windows engineers allowed the user to pop up a menu with the keyboard and keep it open, it was probably easy for them to support the two-click method.)

The two-click method is trivial to implement, but if we want to achieve the same usability of an OS menu, we’ll want to also support the drag method. That’s hard to do, which is probably why most web apps don’t support it. (The various Google Suite apps are a notable exception.) A few of the more interesting problems:

This is all hard, but still doable, so we’re giving it our best shot. If you’re on a laptop, try opening our MenuButton demo and confirming that the menu component feels like an OS menu.

For completeness, I should point out that many web menus also handle an additional means of selecting a menu item with a mouse: the menu opens on hover, after which the user only needs to click once on the desired menu item. I hesitate to mention that approach, however. It’s my personal belief that hover menus are a usability disaster: they invariably appear when they’re not wanted, and disappear when they are wanted. The hover approach does have a distinct advantage, in that it lets the top-level menu heading itself serve as a clickable link. But I think that advantage comes at a steep usability cost.

Our two-click approach for menus should generally work on mobile devices, with some minor changes. Generally speaking, mobile menus appear when a tap ends and force use of the two-click method described above. To ensure the menu responds instantaneously, we must enable fast-tap behavior by applying the CSS touch-action: manipulation to the relevant elements.

If reading this on your phone, try opening our MenuButton demo and tap around. The menu should both appear and disappear as soon as you complete a tap.

Keyboard support and accessibility

As with all Elix components, we strive for excellent keyboard support. This benefits all users that want to use a keyboard and improves universal accessibility.

We allow users to invoke a menu button by pressing Space. The user can navigate the items in the resulting Menu with the full set of keyboard navigation keys supported by KeyboardDirectionMixin, KeyboardPagedSelectionMixin, and KeyboardPrefixSelectionMixin. Without writing any new code, those mixins give Menu support for Up/Down keys, Page Up/Page Down keys, Home/End keys, and prefix selection (e.g., type “Z” to select “Zoom”).

In making our menu components accessible via ARIA, we were helped by this excellent Inclusive Components article on Menus & Menu Buttons. The whole Inclusive Components series is worth a read.

While our Menu component generally behaves like our ListBox, the accessibility rules for menus are different than lists. The role attributes involved are different, for one thing. Another way in which menu accessibility is different than that for lists is that the overall list element can take the keyboard focus, whereas the browser expects a menu to put the keyboard focus on an individual menu item.

Happily, our mixin-based approach to components was hugely helpful in letting us create a Menu component that worked mostly like our ListBox component, but with some differences. Rather than subclassing ListBox or creating a common base class (as we might have in a traditional class hierarchy), we simply copied over the set of mixins ListBox was using, dropped the ones we didn’t need, and then created an AriaMenuMixin for menus to replace the AriaListMixin which ListBox needs. We end up with a Menu that cleanly shares 90% of the code from ListBox without any class hierarchy entanglements.

Customizability

For styling and general customizability, all these menus components have replaceable parts. So you can use a MenuButton, but swap out the elements it uses by default with our own custom elements. You could:

Bonus: a customizable select element

With our MenuButton component in hand, it was easy to create a DropdownList variation that shows the selected value as the menu button’s label. When the user makes a selection from the menu, the button label updates to match.

This effectively lets you use DropdownList as a customizable version of the built-in HTML <select> element. The native <select> can only cope with text choices, but DropdownList can handle arbitrary content — including custom elements, of course — as content in both the menu button and the menu items. See this customized dropdown list demo for an example.

Interestingly, the native <select> is a place where some users may use the drag-to-select method to make a selection — even if they’re the type of user that normally selects from an app’s menu bar using the two-click method. In other words, all the work we did to build a menu button with great mouse support also makes it possible for us to deliver a dropdown list (<select>) with great mouse support.

These details are a pain!

Getting all these details correct takes far too much time. Which is precisely why no app team should try to build a menu component from scratch! The only sane way to achieve OS-quality menu components for web apps is to share code — to pour the attention of an open component library community into menu components that everyone can use. That is why Elix exists.