We've released v2.2 of the Elix web components library, which includes some new components for menus:
MenuButton
variation that's effectively a completely customizable <select>
element.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.
focusOptions
parameter.) So we'll have to complete all our menu layout and rendering before we try to move the focus into the menu.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.
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:
mousemove
on the menu button and do our own hit-testing to figure out whether the user is currently dragging the mouse over a menu item — and, if so, select that item.mouseup
event. So we'll need to listen to mouseup
events on the document
too.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.
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.
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:
Menu
that contains the menu items with an element that lays out items as pie slices instead of the usual vertical orientation.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.
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.