Here at Pixel & Tonic, we've been working hard on improving the accessibility of Craft CMS. Some of our most impactful control panel accessibility improvements come from improving the UI used throughout the control panel.
A recent example of this was fixing up the “My Account” menu, which used a custom listbox select component. It didn’t meet our success criteria after screen reader testing, so we built a new component to improve this and other action menus in the control panel.
Accessibility evaluation: the original menu
First, let’s talk about the reason behind this change. Our ultimate goal is for Craft 4 to meet WCAG 2.1 at the AA conformance level.
To meet this goal, we’ve been evaluating the control panel according to the WCAG Success Criteria. When we looked at the global layout of the CMS, it was apparent that the My Account dropdown was at odds with two Success Criteria:
Let’s dive into each of these and talk about why.
1.3.1: Info and Relationships
Using semantic HTML can take you a long way towards meeting SC 1.3.1. This success criterion requires that: “Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.”
Put plainly, relationships that are implied visually should also be implied to screen reader users. The problem with our menu was that sighted users and screen reader users were getting two very different ideas of the UI and how it functioned.
Visually, the component looked like a button that controlled the display of a link list. But from a screen reader user’s perspective, the My Account “button” was actually a listbox widget, closely resembling an HTML select input.
<button
type="button"
id="user-info"
class="btn menubtn"
aria-label="My Account"
title="My Account"
data-menu-anchor=".header-photo"
tabindex="0"
aria-controls="menu.Garnish863404592"
aria-haspopup="listbox"
aria-expanded="false">
…
</button>
In this case, the semantics weren’t quite adding up.
If something looks and acts like a button, it should probably use a button tag. And if something looks like a link and acts like a link, it should probably use an anchor tag.
Which leads us to the key takeaway…
Don’t emulate links
A quick way to fail at SC 1.3.1 is by emulating anchor links using non-anchor elements. Specifically, failure F42 says that you shouldn't attach JavaScript event handlers to elements to emulate links.
In this case, each anchor inside the menu was assigned the option role; event handlers would trigger a new page load when one of the options was selected. The fact that the options functioned as links was never communicated to screen reader users.
// Is this an option or a link? 🤔
<li>
<a
href="http://craft3.nitro/admin/myaccount"
class="flex flex-nowrap"
role="option"
tabindex="-1"
id="null-option-0">
<div class="flex-grow">
<div>admin</div>
<div class="smalltext">lupe@pixelandtonic.com</div>
</div>
</a>
</li>
This was a clear failure of 1.3.1 that we needed to fix.
3.2.2: On Input
The component’s structure was one piece of the puzzle, but it also needed some behavioral work. The 3.2.2 criterion states that: “Changing the setting of any user interface component does not automatically cause a change of context unless the user has been advised of the behavior before using the component.”
Put simply, updating the value of a form control shouldn’t result in jarring changes like:
- Going to a new page
- Opening a new window
- Moving focus to a new component
- A significant rearrangement of the page content
This leads us to the next problem with our original menu.
Don’t surprise your user
Choosing an option from the My Account dropdown triggered an immediate (and possibly unwanted) page redirect. And we weren’t giving users the tools or information they needed to decide if they should move forward with that redirect.
The WCAG guidelines specify several techniques that we can use to keep users from triggering unwanted changes when updating controls like this.
One technique is to provide an explicit submit button, as shown in technique H32. In our case, we could have provided the user with a “Go to page” button. This would have given them the choice of whether or not to move forward with an action like signing out.
Another technique, as specified in G13, would be to provide a warning that making a selection triggers a page redirect. By linking the warning with the dropdown, the user would learn about a potential change of context before choosing to update the control.
Because we opted to use a different navigation pattern, we didn’t implement either of these techniques. But it can be useful to have these potential tools in mind when building out your interfaces.
With a clear idea of our structural and behavioral problems to solve, we can start to figure out what a solution might look like.
Choosing a design pattern
Rather than try and reinvent the wheel, we turned to the W3 for guidance on how to rebuild this component. Specifically, the WAI Authoring Practices 1.1 and the related Design Pattern Examples are invaluable resources for learning how to build accessible interfaces.
These resources document how different kinds of widgets should be built from an accessibility perspective, and they explain everything from modal dialogs to image carousels.
For each type of widget, you can learn about recommended markup, expected keyboard interactivity, ARIA, and much more. Also, working examples are usually provided so that you can see each component in action!
For this component, we decided to use the Disclosure (Show/Hide) pattern.
According to the WAI Authoring Practices 1.1 document, a disclosure is “a button that controls visibility of a section of content. When the controlled content is hidden, it is often styled as a typical push button with a right-pointing arrow or triangle to hint that activating the button will display additional content.”
While lacking a right-pointing arrow, the My Account button clearly indicates that it controls the display of a menu. Overall, this pattern seemed perfectly suited for this — and other — menus in the control panel. We specifically chose to follow the instructions for implementing a Disclosure Navigation Menu.
Building the new menu component
Now we’ll talk about how all of the pieces came together to form a more accessible My Account menu.
Communicating interactivity
It’s been said a thousand times: the first rule of ARIA is do not use ARIA.
The ARIA specification gives us additional HTML attributes we can use to convey semantic and accessibility information to users of assistive technology. But this power can be used for good or evil. When used incorrectly, it can cause more accessibility issues than it solves.
Most of the time, you should opt for native HTML elements. They come packed with built-in semantics, keyboard support, and more. However, more complex interfaces and widgets may require the extra clues that ARIA provides.
By adding some careful attributes to the “My Account” button, we were able to communicate helpful information to assistive technology users.
Here’s the updated button markup; only the outer HTML and relevant attributes are shown.
<button
id="user-info"
aria-controls="account-menu"
aria-label="My Account"
aria-expanded="false">...</button>
This button controls something else
If you happened to stumble upon a random button, you might not understand its purpose. Its name alone might not provide enough information about what it does.
Does it submit a form? Does it launch a missile?
By applying the aria-controls
attribute on the button, we’re telling users that its purpose is to control the visibility of another component.
This button has a state
The aria-expanded
attribute helps us tell the user what state the button is in. Without this attribute, a screen reader user would have to infer the button’s state based on other clues in the UI.
We want to make things simpler for our users to understand, and adding the aria-expanded
attribute to our button does just that. Then we toggle the value on open and close.
Focus management
When it comes to focus management, the key idea is: don’t lose your user. Poor focus management can completely block a user from using your app or website. For example, if a user activates a modal and the focus doesn’t move into it, your page is essentially unusable for someone navigating with a keyboard or other assistive device.
Activating the menu
When a user toggles a button to open a link list, one of two things should be true:
- The menu container should be the next element in the DOM. If the user hits the TAB key after opening the menu, the next focused element should be the first menu link or first “focusable” item inside.
- If the menu isn’t immediately after the trigger button in the DOM, toggling the menu control should move focus to the first link or first “focusable” item.
By default, I’ll usually move focus to the first focusable element inside the container on open. Regardless of the DOM order. This item might be a link, a button, a form field — anything that can be focused via the keyboard.
Here’s that focus management in action:
This “focus the first focusable element” technique isn’t mentioned in the Disclosure Navigation example docs, as they only describe a case where the menu immediately follows the trigger button in the DOM. However, I’ve seen the technique implemented in other accessibility resources, so I felt comfortable using it for added focus management.
Closing the menu
When you close the menu, it’s necessary to move focus back to the toggle button. This keeps the user’s focus from becoming lost to the void — or reset to the top of the document.
In this case, when the menu is closed, keyboard focus is shifted back to the My Account button.
Keyboard commands
It’s also essential to understand how your widget should function in terms of keyboard support. Luckily for us, the ARIA Authoring Practices examples include useful documentation about keyboard support.
In this case, we referenced the Keyboard Support section of the Disclosure Navigation Menu example.
Because we’re using semantic link elements inside the menu, TAB and SHIFT+TAB keypresses in the menu are handled natively. Keyboard users can navigate forward and backward between the menu items because the anchor tags provide all the keyboard support we need. That’s the power of semantic HTML!
We also chose to include the optional support for navigating menu items via the arrow keys, as this matched the previous menu behavior.
An escape key handler was also included so that users can close the menu without having to navigate back to the toggle button.
The new account menu
Now that you understand all the individual parts of the Disclosure Navigation pattern, let’s see how it stacks up for users navigating with a screen reader.
If you’re running Craft 3.7.17 or above, you can see the new account menu in action. This is just one of the many components we’ve been testing and retrofitting for a more accessible experience, and you’ll see that same pattern used more in upcoming releases.