Short Answer
The checkbox hack is not a good practice, especially for navigational menus and is not semantically correct, generally causing accessibility nightmares.
As "JavaScriptless" users are around 1.3% of users you should provide a usable but perhaps ugly version of the menu for them and make sure that for the majority of users everything is semantically correct.
Please note: The advice here is for navigational drop-downs, if this is for a complex application where the drop-down triggers functions on the current page rather than navigation then other patterns may be better.
Long Answer
I would normally address the question being asked but you are making things very difficult for yourself and I think it would be better to provide an alternative.
I have listed the reasons why not to use the checkbox hack and provided an explanation of a much simpler, more robust and much more accessible solution which should hopefully help you going forward.
First, the bad news
I hate to say it but you will never make the above accessible without JavaScript and your current implementation has quite a few accessibility issues.
First of all aria-checked
needs to be toggled and you can only do that with JavaScript. Also a checkbox is not a logical / semantically correct element to use here and does not convey the right information to screen readers. If you then add role="button"
to counter this then aria-checked
is not a valid attribute.
Secondly the "checkbox hack" is not intended for navigational menus it is intended to be used for complex menus as part of an application, it is still not a good pattern to use then and should only be used if you are really struggling to make other options work.
Thirdly pseudo elements (your "+" symbol) are not focusable and a lot of screen readers ignore them / don't behave well with them, so that is a big accessibility problem.
Fourthly anywhere where you are using <a href="#"
is an accessibility anti-pattern. Anchors should only be used for navigation, <button>
elements are for same page actions / functions. This is down to how they are announced to screen readers and expected behaviour. If you use a hyperlink it must contain a full and valid href
either to an anchor on the current page or an entirely new page.
There are other issues but hopefully you get the idea!
Never fear, there is an easier way to do this!
Your main concern is that your menu works without JavaScript, which is causing you to choose hacks over the best and easiest ways to do things.
Here is the simplest way to create an accessible experience for all.
- Create an HTML sitemap with anchors at key points.
- Make the "button" an anchor and point the
href
to the relevant part of the sitemap.
- Use JavaScript to intercept the request and open the drop-down menu / toggle
aria
attributes for the majority of users who have JavaScript enabled.
This way there is still a way to navigate the site if JavaScript is disabled, but for majority of users you have a drop-down menu.
Rough example
In the below example I have covered most accessibility issues.
No JavaScript
The raw HTML is valid and points to an HTML sitemap (which in the example is simulated with anchors further down the page, you should obviously have this on another page!).
If your sitemap is particularly large then you should use ids at the relevant parts of the page and link to those anchors directly (i.e. yoursite.com/sitemap#a-particular-category-or-main-menu-item). I have included this as part of the example as well.
To test the above I have included a checkbox that removes all the relevant event handlers and aria attributes so you can experience the "javaScriptless" experience.
With JavaScript
If JavaScript is available then we add the relevant role="button"
to the link, we also add the aria-expanded
attribute that we can later toggle to tell screen reader users if the menu is open or not and the aria-haspopup
attribute so they know that the "button" will open a popup.
As we have told screen readers that the hyperlink is now a button with role="button"
we allow them to activate the "button" with the space key as that is expected behaviour.
Finally they can close the drop-down with the Esc key as a "nice to have", this is not essential for navigational menus as they should not trap focus but I always like to add it, although I haven't dealt with returning focus to the parent item (something for you to consider).
I also ensured that the tap target was 44px by 44px to ensure it passes 2.5.5 tap target size as that was another issue with your example.
As a "+" is not very informative for screen reader users I also added some visually hidden text to explain what the toggle button does. I also toggle this button text depending on whether the drop-down is open or closed. This is done at the same time as toggling aria-expanded
to make things perfectly clear for screen reader users.
There may be things I have missed in the following example so please test it thoroughly before using it in production. Also apologies I wrote this as I thought of things that needed addressing so the code is probably a bit messy.
var toggles = document.querySelectorAll(".menu-toggle");
// add the relevant aria, role and handler
var init = function(){
for(var x = 0; x < toggles.length; x++){
var el = toggles[x];
el.setAttribute("role", "button");
el.setAttribute("aria-expanded", "false");
el.setAttribute("aria-haspopup", "true");
el.addEventListener('click', openToggle);
el.addEventListener('keydown', keydownHandler);
document.addEventListener('keydown', closeAllHandler);
}
}
init();
function keydownHandler (e) {
//space key
if (e.keyCode === 32) {
openToggle(e);
}
}
function closeAllHandler(e){
// esc key
if (e.keyCode === 27) {
var openItems = document.querySelectorAll(".open");
for(var x = 0; x < openItems.length; x++){
openItems[x].classList = "";
}
}
}
//handler for when the "+" button is pressed, the second parameter is a quick way to recycle the function as a close function
function openToggle(e, close){
e.preventDefault();
var self = e.currentTarget;
var dropDown = getNextSibling(self, "ul");
if(dropDown.classList == "open" || close){
dropDown.classList = "";
self.setAttribute("aria-expanded", "false");
self.querySelector('.icon').innerHTML = "+";
self.querySelector('.toggleText').innerHTML = "show submenu";
}else{
dropDown.classList = "open";
self.setAttribute("aria-expanded", "true");
self.querySelector('.icon').innerHTML = "-";
self.querySelector('.toggleText').innerHTML = "close submenu";
}
}
// helper function to grab next sibling by selector.
var getNextSibling = function (el, sel) {
var sib = el.nextElementSibling;
if (!sel) return sib;
while (sib) {
if (sib.matches(sel)) return sib;
sib = sib.nextElementSibling
}
};
//////////////////////////////DEMO ONLY NOT NEEDED IN PRODUCTION///////////////////////////////////
//just for the demo, allows usa to simulate JavaScript being switched off
document.getElementById("jsActivated").addEventListener('change', adjustJS);
function adjustJS(e){
if (!e.target.checked) {
init();
} else {
destroy();
}
};
// just for the demo, used to remove event listener and role
var destroy = function(){
for(var x = 0; x < toggles.length; x++){
var el = toggles[x];
el.removeAttribute("role");
el.removeAttribute("aria-expanded");
el.removeAttribute("aria-haspopup");
el.removeEventListener('click', openToggle);
el.removeEventListener('keydown', keydownHandler);
}
}
.has-submenu>ul{
display: none;
}
.has-submenu>ul.open{
display: block;
}
li{
padding: 10px;
}
.visually-hidden {
border: 0;
padding: 0;
margin: 0;
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
clip: rect(1px, 1px, 1px, 1px); /*maybe deprecated but we need to support legacy browsers */
clip-path: inset(50%); /*modern browsers, clip-path works inwards from each corner*/
white-space: nowrap; /* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */
}
.menu-toggle{
height: 44px;
width: 44px;
outline: 2px solid #666;
display: inline-block;
line-height: 44px;
font-size: 25px;
text-align: center;
text-decoration: none;
margin-left: 10px;
}
<label>Simulate JavaScript Off?
<input type="checkbox" id="jsActivated"/>
</label>
<h1>Example Accessible Menu</h1>
<nav aria-label="Main Navigation">
<ul>
<li class="has-submenu">
<a href="https://example.com/some-category">Some Category</a>
<a class="menu-toggle" href="#sitemap-item1">
<span class="icon">+</span>
<span class="visually-hidden">
<span class="toggleText">show submenu</span> for "Some Category"
</span>
</a>
<ul>
<li><a href="https://example.com/some-category/item-1">Some Category Item 1</a></li>
<li><a href="https://example.com/some-category/item-2">Some Category Item 2</a></li>
</ul>
</li>
<li class="has-submenu">
<a href="https://example.com/this-is-a-different-category">This is a different category</a>
<a class="menu-toggle" href="#sitemap-item2" aria-expanded="false" aria-haspopup="true">
<span class="icon">+</span>
<span class="visually-hidden">
<span class="toggleText">show submenu</span> for "This is a different category"
</span>
</a>
<ul>
<li><a href="https://example.com/this-is-a-different-category/item-1">Different Category Item 1</a></li>
<li><a href="https://example.com/this-is-a-different-category/item-2">Different Category Item 2</a></li>
</ul>
</li>
</ul>
</nav>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<strong>Scroll up to get back to menu, this is to simulate another page</strong>
<h2>The sitemap located on another page</h2>
<ul>
<li id="sitemap-item1"><a href="https://example.com/some-category">Some Category</a>
<ul>
<li><a href="https://example.com/some-category/item-1">Some Category Item 1</a></li>
<li><a href="https://example.com/some-category/item-2">Some Category Item 2</a></li>
</ul>
</li>
<li id="sitemap-item2"><a href="https://example.com/this-is-a-different-category">This is a different category</a>
<ul>
<li><a href="https://example.com/this-is-a-different-category/item-1">Different Category Item 1</a></li>
<li><a href="https://example.com/this-is-a-different-category/item-2">Different Category Item 2</a></li>
</ul>
</li>
</ul>
<strong>Scroll up to get back to menu, this is to simulate another page</strong>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>