I have created a JavaScript-free, HTML- and CSS-only drop-down menu using the checkbox hack. I am trying to avoid using JS as some users disable it. For touch-screens and mouse it works fine, but I am attempting to make it work so that anyone using keyboard navigation can use it.
It does work more or less:
The [Enter] key opens links and [spacebar] opens some (see further) sub-menus using checkboxes which are invisible to the naked eye by using the class opacity: 0;
.
However, I have two contradictory problems with keyboard navigation:
The <a>
tags and pseudo ::after
elements associated with some checkboxes focus visibly with a faint dotted highlight but do not open when the spacebar is pressed.
When using a <span>
tag instead of an <a>
tag, the menus open when the spacebar is pressed but they are not visibly 'focused' with the dotted outline.
Also, the 'invisible' checkbox is positioned behind the ::after
pseudo element of '+' but, because it's invisible, there is no focus
outline.
Here is my simplified code with examples of the different combinations I have tried:
ul.menubar {
padding: 1rem 0 0 0.5rem;
}
[id^=chk] {
opacity: 0;
}
[id^=chk]+ul {
display: none;
}
[id^=chk]:checked+ul {
display: block;
}
label.link::after,
label span.separator::after,
label a.separator::after {
position: relative;
content: "+";
right: -1.25rem;
top: 0;
}
a {
text-decoration: none;
}
li {
line-height: 2.5rem;
font-size: 1.25rem;
list-style: none;
}
li a,
li span,
li label,
li input,
.inline {
display: inline;
position: relative;
}
<body>
<nav>
<ul class="menubar" role="menubar">
<li>
<div class="inline"><a href="#" role="menuitem" tabindex="1">Item link plus dropdown using span. + works but no keyboard focus | </a>
<label for="chk-101" class="link" title="Item 1 link" aria-label="Item 1 link"></label></div><input id="chk-101" type="checkbox" class="chkbox" tabindex="1" aria-checked="false">
<ul>
<li><a href="#">Dropdown 1a</a></li>
<li><a href="#">Dropdown 1b</a></li>
</ul>
</li>
<li><a href="#" role="menuitem" tabindex="1">Item link plus dropdown using 'a' tag with href; + works but not keyboard focused | </a>
<label class="link" for="chk-102"><a href="#" class="link" tabindex="-1" title="Item 2 link" aria-label="Item 2 link"></a></label><input id="chk-102" type="checkbox" class="chkbox" tabindex="1" aria-checked="false">
<ul>
<li role="menuitem" tabindex="1"><a href="#">Dropdown 2a</a></li>
<li role="menuitem" tabindex="1"><a href="#">Dropdown 2b</a></li>
</ul>
</li>
<li>
<label for="chk-103"><span class="separator">Separator with span - does not focus</span></label><input id="chk-103" type="checkbox" class="chkbox" tabindex="1" aria-checked="false">
<ul>
<li role="menuitem" tabindex="1"><a href="#">Dropdown 3a</a></li>
<li role="menuitem" tabindex="1"><a href="#">Dropdown 3b</a></li>
</ul>
</li>
<li>
<label for="chk-104"><a href="#" class="separator" tabindex="1">Separator with href - focus does not respond to spacebar</a></label><input id="chk-104" type="checkbox" class="chkbox" tabindex="-1" aria-checked="false">
<ul>
<li role="menuitem" tabindex="1"><a href="#">Dropdown 3a</a></li>
<li role="menuitem" tabindex="1"><a href="#">Dropdown 3b</a></li>
</ul>
</li>
<li>
<label for="chk-105"><span class="separator" role="checkbox" tabindex="1" aria-checked="false" aria-label="Separator 2">Separator with span role="checkbox"; focus but does not respond to spacebar</span></label>
<input id="chk-105" type="checkbox" class="chkbox" tabindex="-1" aria-checked="false">
<ul>
<li role="menuitem" tabindex="1"><a href="#">Dropdown 3a</a></li>
<li role="menuitem" tabindex="1"><a href="#">Dropdown 3b</a></li>
</ul>
</li>
</ul>
</nav>
</body>
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.
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.
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!
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.
href
to the relevant part of the sitemap.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.
In the below example I have covered most accessibility issues.
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.
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/>