How button groups should work for keyboard users

I’ve written about the difference between navigation and menubars, where I mention that menubars should be few and far between on the web. But menubars are part of a larger family of ‘button groups’, which are fairly common.

Some members of the button group family are:

Menubar

Menu

Toolbar

Tablist

So how should button groups behave when using the keyboard?

Some quick, oversimplified examples

Before I dig into the commonalities of button group behaviour, it helps to understand what each type of button group is and how it might be marked up.

Menubars

MDN Docs describes menubars as:

a presentation of menu that usually remains visible and is usually presented horizontally

Each item usually contains its own menu; think those File/Edit/View/etc. menubars at the top of Google Docs documents. A menubar’s markup might look something like:


File

Edit

View




This is an illustration and far from production code! You’re going to want a bunch of other things on the menu items, like:

aria-haspopup to let screen reader users know if a item has a menu of its own (it probably does)

aria-controls to make create a relationship between the menubar button and its menu

Toolbars

Toolbars are similar to menubars but are usually more focused on a particular purpose, like formatting text. MDN Docs describes toolbars as:

a collection of commonly used function buttons or controls represented in a compact visual form.

Here’s how the markup might look:


Bold
Italic
Bulletted list
Numbered list



Again, this is a huge oversimplification; among other things, you’re going to want:

aria-controls on the toolbar to hook it up to its text editing area

icons (SVGs, or some other kind of image) in place of the button text (one of the few instances where icon-only buttons are probably okay to use)

aria-pressed to communicate whether a format has been applied or not

type="button" on all those elements, to stop them submitting the form when pressed

Menus

Menus are found in all sorts of places: sometimes inside menubars and toolbars, sometimes as submenus inside other menus; sometimes on their own, like a right-click context menu or attached to Actions buttons on each item in a list. MDN Docs describes menus as:

a type of composite widget that offers a list of choices to the user.

Here’s how you might mark up a menu:

Actions

Move up
Move down
Edit
Delete


Menus are pretty straightforward, so all I’d add is that you’ll want aria-expanded on the trigger button, if there is one. Oh, and if you’ve got submenus you’re going to want aria-level to inform screen reader users about how deep in the menu hierarchy they are, as well as some aria-haspopup, aria-controls, of course.

Tablist

MDN Docs describes a tablist like this:

The tablist role identifies the element that serves as the container for a set of tabs.

Pretty straightforward: a group of tabs. Here’s some example markup:


Tab 1
Tab 2
Tab 3




Again, that’s oversimplified, so in your working code you’ll be reaching for:

aria-controls on each tab to match the id of its tab panel

aria-selected to tell screen reader users whether or not the content for the tab they’re on is currently in view

the hidden attribute on the currently unselected tab panels

Keyboard behaviour

Now you know what a button group is, it’s important they all work in the same way using the keyboard; especially as some can be contained within others, like menus inside menubars and toolbars. Buckle up as this gets complicated!

A single tab stop for keyboard users

Once a keyboard user has tabbed onto the button group, they would then move from button to button using the arrow keys, rather than further tab presses. Another press of the ⇥ (tab) key should take focus off the button group and onto the next interactive element in the document.

So on top of the extra markup I mention after each example, you’re also going to want tabindex attributes on each button. JavaScript should listen for arrow key presses and clicks to shift both the 0-value tabindex and the focus (via the focus() method) around, while ensuring the non-focused buttons have tabindex="-1".

Worth mentioning too that when tabbing onto a button group when no button has been selected, the first item should get focus. If a button has been selected, for example a tab in a tablist, focus goes directly to it.

Arrow keys to get around

Speaking of key presses, once a button group has focus the arrow keys are the way to get around.

In horizontally laid-out groups the → (right) and ← (left) keys move between the buttons; in vertically laid-out button groups it’s ↓ (down) and ↑ (up).

Of course these can be used in combination, for example a menubar where → and ← move between items, and ↓ and ↑ move focus up and down one of its menus.

If each menu in a menubar or toolbar doesn’t open automatically on focus, pressing ↓ should open the menu and move focus to its first item. ⏎ (Return) or Space should do the same. ↑ usually places focus on the last item in the menu.

Whether ↓ when on the last menu item circles focus back up to the first or just stays put is up to you. I’d probably go with the last item being a dead-end to avoid risk of any disorientation. Ditto for horizontally laid-out button groups: → when on the last item (or ← when on the first) could move focus to the first (or the last), but I wouldn’t do that.

When a menu item has a submenu, it should be opened with → (or ⏎ or Space) and focus should go to the submenu’s first item. ← when in a submenu should close it and put focus back on it’s parent button.

Bells and whistles

You could even add extra keyboard shortcuts, for example Home and End could move focus to the first and last items in a menu. ⌥ (Option) + ↑/↓ also does that on macOS.

Here’s another one that you’d find on macOS: ⌘ (Command) + ↓/↑ jumps from one section within a menu to the next.

In a menubar menu, and on a menu item without submenu, → could move focus to the next menu in the menubar. And ← would do the same in the opposite direction.

Escape

Menubars, toolbars, and tablists are always on view, but menus are normally hidden until triggered; the Esc key should close an open menu and put focus:

back on the menu’s parent button if triggered by a button on-page, or as part of a toolbar

where the cursor is in the document’s editing area, if part of a menubar

Screen reader users

Screen reader users will first hear the role of the button group, which will tell them that the button group style interaction pattern is required, rather than their normal screen reader navigation keys.

Orientation

So that screen reader users know which arrow keys to press (→/← or ↓/↑) You could use aria-orientation to explicitly set the horizontal/vertical orientation of the button group, but this is:

Not all that well supported across screen readers

Probably implicit in the button group type anyway (menubars, toolbars, and tabs are usually horizontal, menus normally vertical)

Complex

As you can see, button groups are fiddly components, requiring an awful lot of JavaScript to drive the various keypresses. But there’s some good news:

Menubars are almost never what you need

Toolbars aren’t all that uncommon

Menus are fairly low complexity, especially when kept to one level

Tablists can (and often should) be avoided by using alternate designs, but at least they’re the simplest form of button group if you need to use one

So for when you do need to use a button group, now you know how they should behave using the keyboard.