Making accessible menus

I wrote a theme with a menu that is responsive and uses only CSS, by putting extra markup before each submenu. I used an input and its label with CSS to trigger the opening and closing of the submenus.

It works pretty well with the keyboard also, except that one extra stop when tabbing through the links. I was going to fix it with tabindex=-1, but I’m not sure how that would affect swiping on screen readers (the equivalent of tabbing with keyboard).
I’m wondering if anyone knows how to make a menu fully accessible without javascript.

One thought I had was using details and summary elements so the browser can handle the open and close itself, but I wondered about a11y for those elements and know that not quite all browsers support them.

Any other ideas?

Nested uls, and the child lists only show when a parent item has the :hover or :focus state.

There shouldn’t be anything else needed, unless I am missing something?

Read some more and this won’t work with keyboard navigation by default. Also needs ARIA roles in order to indicate the presence of submenus.

The new :focus-within pseudo-selector in CSS4 will help: Solved with CSS! Dropdown Menus | CSS-Tricks - CSS-Tricks

If support for IE 11 or non-current Edge versions is needed, then some JavaScript will be needed. Hopefully it should be minimal.

Yes, missing the a11y part. The :focus state is the key thing. First, the focus state should be visibly different. Second, it should be keyboard navigable. Third, it should be screen reader navigable.

You can use :focus-within for modern browsers. I do this, but it limits the supported browsers.
My solution with the input field makes the mobile version work well, where you click to open the submenu, but it makes an extra tab stop for the keyboard navigation on desktop. I haven’t been able to test whether using tablindex=-1 makes the screen reader swipe skip the input or not.
So I wondered if using other elements that the browser handles already would work better.

This is not quite accurate as far as I understand things. ARIA is just for putting state into the accessibility tree. The submenus exist already in the document tree, so there is no real need to do anything with ARIA. (I think)

1 Like

You might not need to specify an ARIA role. If you are using regular markup, nav should be enough to indicate a menu, and then the menu items themselves are taken care of by the use of ul, li, and a tags.

But when you get to the sub-menu (and assuming that you intend to “show” the submenu to a screen-reader), then I definitely would recommend adding aria-label="submenu" to enable the screen-reader to announce this to the user. It sounds like you will also need some ARIA attributes like has-popup, aria-expanded, and/or aria-hidden. See https://heydonworks.com/practical_aria_examples/

Using tabindex="-1" is good practice if you don’t want the user to be able to tab into the element, but still want it to be capable of taking focus by some other method.

2 Likes

I came to a different conclusion when I previously read that page and the page it points to as an update. The update page describes two types of menus. One is a list of links like we are used to and the other is a “true” menu like is found in application software, which needs ARIA and more javascript to power it.
At the end is a checklist:
“Don’t use ARIA menu semantics in navigation menu systems.”
but also
"Never sacrifice usability in the pursuit of JavaScript-free solutions.:

So, I’m still trying to figure it out. Apparently, using an input changes the key needed to activate it (space instead of Enter), and also what the screen reader announces.
I wish the browser had a built-in element that handled it all.

which is why you need js to handle it, it doesn’t hurt to add a few lines of js to solve that.

1 Like

I agree with @Devrealm_Guy. There’s nothing wrong with using JavaScript if it’s the right tool for the job.

I obviously don’t know exactly how your menu works. But I can say that users relying on screen-readers have to deal with unbelievable barriers. If you are in any doubt at all, add the ARIA attributes. You lose nothing, and (depending on the screen-reader) you will usually be helping.

2 Likes

I’m looking for specifics. Got any?
And in your mind, what is “it”?

That’s not what the article you mentioned says. So it’s a bit confusing. As I said, I’m looking for specifics, not just “add some ARIA”.

If there is a way to make the menus generated by core more accessible, I would like to be able to apply that fix in core. If that means an additional JS loaded on the front end, I would think core would need to do it, instead of each theme doing it. Of course, it should be enqueued, so that themes can dequeue it and use their own version. But if core contained the workable solution, so many sites would be more accessible just by updating to that version.

1 Like

I can’t be more specific if I can’t see your menu.

1 Like

Well, my menu uses the core nav walker, although I added additional input and label preceding each submenu.
But the real question is “What is the generic case of best accessibility for core menus?”

If you use jQuery UI menus, you’ll see that they have all the accessibility stuff built-in. Which, of course, is something that so many people overlook when they talk about replacing jQuery with vanilla JavaScript.

And jQuery UI is bundled with core.

Is the whole menu visible, or are some bits visible only when certain events happen? If it’s the latter (which is what I had initially inferred), then I think this passage on the page I referred to covers it:

Simple solutions are good solutions. For this basic implementation of navigational “dropdown” submenus, the aria-haspopup alerts you to the presence of a submenu. The addition of an aria-label with a value of “submenu” just confirms it is a submenu you are entering as the first item is focused.

I also don’t see anything on that page that says not to use ARIA attributes.

I frequently get confused by ARIA and you can really get tied up in knots with it. I’m not sure if this helps @joyously but I always using Lightning in Chrome to run basic accessibility tests and also use the menu (and other) examples on the W3 WAI website as reference.

More examples here:

Ultimately, if Lightning says what I’ve done is OK and I’ve followed best practices from the WAI site, then I’m reasonably happy with that.

In my case, the menu is visible on narrow windows when you click the submenu indicator. On wider windows, no click is needed for the submenu to show on hover or keyboard navigation.
I’ve seen many implementations that show the entire menu on click of a menu button on narrow windows.

It’s the line I quoted, from the update of the page you linked to.
“Don’t use ARIA menu semantics in navigation menu systems.”

Maybe I should be thinking of it in terms of one set of markup for normal users and a different set for screen readers, but they have to work with keyboard and touch.

@joyously The issue is solved out of the box in Twentyseventeen theme, a little bit of refinement here Handles toggling the navigation menu for small screens and e - Pastebin.com (js code) This is what I use in all of my themes - handles menu toggling/accessibility on small devices and also solves the issue of tabbing through links.

To solve the ScreenReader issue:
Replace the theme name with yours, something like this

( 'aria-label', chickenbrainyScreenReaderText.expand );
Replace all the chickenbrainy appended with your theme name (they are in the js above)

Even if you replace all that, it won’t work, as you need to configure it to soothe your theme menu, this is how:
The Screenreadertext is appended, so it lets js knows you have some translatable strings somewhere, those translatable strings are what the Screenreader will read (functions.php in this case)

You can’t translate strings in js as far as I know in Wp/Cp, you have to translate it in a .php file. I assume you are using functions.php!

Define the translatable strings, and then pass those strings to the js in the defined variable, which is yourthemeScreenReaderText, use the localize_script to solve that

 wp_localize_script(
			'theme-navigation', // label you are sending the strings to
			'themenameScreenReaderText', // var you passed to the js file
                        array (
                                'expand'   => __( 'Expand child menu', 'your-theme-textdomain' ),
				                'collapse' => __( 'Collapse child menu', 'our-theme-textdomain),
                               )

the array contains the two values you are passing on, and can be found in the js above

One more thing thou, there are some classes you need to append to your menu:

if you need the mobile menu toggle to function with the js code use this

<nav id="site-navigation" class="main-navigation nav--toggle-sub nav--toggle-small" aria-label="<?php esc_attr_e( 'Main menu', 'your-theme-textdomain' ); ?>"

those classes are in the js, so you need it if you want it to work,

lastly the button:

<button class="menu-toggle" aria-label="<?php esc_attr_e( 'Open menu', 'your-theme-textdomain' ); ?>" aria-controls="primary-menu" aria-expanded="false">
		<?php esc_html_e( 'Menu', 'your-textdomain' ); ?>
	</button>

Use some CSS to style the toggle, and that should fix either the tabbing not working through links or Screenreader not properly translating or menu accessibility not working properly

Edit
If you don’t mind, you can share how you first implemented it or the code, that way, it would be easier to pinpoint the issue

Edit 2
You likely won’t see the dropdown toggle, and here is why - We didn’t specify a way to add it in the js code, you can use Jquery/javascript to find the menu that has children and anchor inside it, and if it finds it, add the dropdown.

Alternatively, since you said you are using the walker class, you can extend the walker class and add something like this

add_filter('add_dropdown_css_class' , 'my_nav_class' , 10 , 2 );
function primary_nav_menu_dropdown_symbol ($classes, $item) {
   if ( ! empty( $item->classes ) && in_array( 'menu-item-has-children', $item->classes ) ) {
			return $item_output . '<span class="dropdown"><i class="dropdown-symbol"></i></span>';
		}
    return $item_output;
}

I haven’t tested this particular code but that should be appended if the menu has children,
You will notice we aren’t outputing anything, we are just adding span, this is because, it becomes easy to style the inline class “dropdown-symbol” with some CSS - add this Css styles to the dropdown-symbol:

    background: transparent;
    border: solid #000; /* adds black border */
    border-width: 0 4px 4px 0; /* remove top and left */
    width: 60%;
    height: 60%;
    display: block;
    position: absolute; /* you ain't going anywhere */
    transform: translateY(-50%) rotate(45deg);  /* rotate  */

If you also have other levels or sub menu, just re-rotate the deg!

Depending on how you styled your menu, you can also use the top and right properties to align

Ah, not from that page at all, then. There’s a reason I linked to the page I did and not to that. It’s because there’s what I call a “Before” Heydon Pickering and an “After” Heydon Pickering.

As the page to which I linked shows, the former wrote great explanations and code about how to do accessibility well. The latter seems to have gone rogue, thrown his toys out of the pram at the fact that screen-readers behave differently from each other (like the old browser wars), and decided to advocate for doing things entirely his own way.

He has claimed, for example, that the label attribute is a waste of time and space because the label sits next to what it’s labeling anyway. But (a) that’s not always true, and (b) I don’t know how an unsighted reader is supposed to know that it’s a label and not just regular text. You’ve already added it yourself, so you know those things.

As I don’t use a screen-reader, if I could actually find someone who does who supports these new ideas, then I would be prepared to consider them. But I’ve yet to find a single supporter. The best I can find are people who call him “thought-provoking”.

On the other hand, I know plenty of people who continue to use his previous examples of how to do it. I use them on my own sites, several of which have registered blind users, and I haven’t heard of them having any problems.

In your own case, it seems that you probably don’t need to add anything for wider windows because everything is visible from the start and the semantic markup should take care of accessibility.

For narrower windows, you probably want to ask yourself how likely it is that someone using a screen-reader is going to want to view your site like that. If you decide it’s extremely unlikely, then you can just leave well alone.

If, however, you are concerned that someone might narrow the browser width on a laptop or desktop screen, then I’d add some javascript, wrapped in a conditional like if (window.matchMedia('(max-width: 799px)').matches) {, and add some ARIA attributes.

On one site, I have a similar menu issue to the one you describe. As I work in an institution where compliance with the ADA is fundamental, I don’t take any chances. So this is currently the code I use:

jQuery(document).ready(function($) {
	'use strict'; // satisfy code inspectors

	// General Menu
	$('#menu-toggle').on('keyup', function(e) {
		if ((e.keycode || e.which) == 32 ) { // spacebar
			e.preventDefault();
		}
	}, false);
	
	var menuToggle = $('#menu-toggle');
	var menuHeader = $('#menu-header');
	var html = $('html');
	var arrow = $('<span/>').addClass('gen').attr('aria-haspopup','true').attr('aria-expanded','false').html('<span class="gm-icon" tabindex="0">▼</span>');
	var screen = $('<span/>').addClass('screen-reader-text').text('Click to see this group of menu items.');
	var close = $('<span/>').addClass('screen-reader-text').text('Click to close this group.');

	$('.menu-item-has-children').children('a').each(function() {
		$(this).after(arrow);
	});
	$(menuHeader).find('.sub-menu').attr('role','group').attr('aria-label','submenu');

	$(menuToggle).on('click keydown', function(e) {
		if (e.type === 'click' || e.which === 13 || e.which === 32) {
			e.preventDefault();
			$(this).next('div').find('.sub-menu').hide();
			$(menuHeader).toggle('slide').attr('aria-hidden', function(index, attr) {
				return attr == 'false' ? 'true' : 'false';
			});
			$(this).attr('aria-expanded', function(index, attr) {
				return attr == 'false' ? 'true' : 'false';
			}).children('span').toggle(); // toggle icons
				
			if (window.matchMedia('(max-width: 799px)').matches) {
				if ($(menuToggle).attr('aria-expanded') == 'true') {
					$(html).addClass('disable-scroll');
					$(menuHeader).addClass('enable-scroll');
				} // disable scroll on mobile devices except for menu itself
				else {
					$(html).removeClass('disable-scroll');
					$(menuHeader).removeClass('enable-scroll');
				}
			}
		}
	});
	
	$(document).on('keydown', function(e) {
		if (e.which === 27) { // Escape key
			if ($(menuToggle).attr('aria-expanded') == 'true') {
				$(menuToggle).click().focus(); // focus on menu button
			}
		}
	});
	
	$('.gm-icon').append(screen).on('click keydown', function(e) {
		if (e.type === 'click' || e.which === 13 || e.which === 32) {
			e.preventDefault();
			var gen = $(this).parent('.gen');
			var sub = $(this).parent('.gen').next('.sub-menu');

			$(gen).attr('aria-expanded', function(index, attr) {
				return attr == 'true' ? 'false' : 'true';
			});
			$(sub).slideToggle().attr('aria-hidden', function(index, attr) {
				return attr == 'false' ? 'true' : 'false';
			});

			if ($(sub).attr('aria-hidden') == 'false') {
				$(this).text('▲').append(close);
			}
			else {
				$(this).text('▼').append(screen);
			}
		}
	});

	// Prevent tabbing out
	$(menuToggle).on('keydown', function(e) {
		if (e.shiftKey && e.which === 9) {
			e.preventDefault();
			$('#gen-search').focus(); // focus on search
		}
	});
	$('.general-menu li a:first').on('keydown', function(e) {
		if (e.shiftKey && e.which === 9) {
			e.preventDefault();
			$(menuToggle).focus(); // focus on menu button
		}
	});
	$('#gen-search').on('keydown', function(e) {
		if (e.which === 9) {
			e.preventDefault();
			if (e.shiftKey) {
				$('.general-menu li a:last').focus(); // focus on last menu item
			}
			else {
				$(menuToggle).focus(); // focus on menu button
			}
		}
	});
	
});