Ever since Bruce Lawson mentioned trying to create a non-JavaScript version for a mobile menu using the Popover API I've been thinking about how to create a modal menu that can function completely on its own without any JavaScript whatsoever. There are so many reasons why JavaScript might not be available that it's actually quite odd that a component that is used in every website still relies on JavaScript to function.
So what do we need? A dialog to put the menu in (obviously), a button to open the dialog and another button inside the button to close the dialog.
<button>Open menu</button>
<dialog id="menu">
<nav>
<ul>
<li>...</li>
</ul>
</nav>
<button>Close menu</button>
</dialog>
Cool! Of course, we don't see the dialog because it's hidden by default (unless we use the open
attribute) and we can click the Menu button until the cows come home but it's not going to do anything. Normally we would now have to add JavaScript to both buttons to be able to open/close the dialog and that would defeat the purpose of creating a version without scripting. So we're stuck, right?
Well … not really. We have choices.
We can use the Popover API to toggle the dialog visibility but this has one big caveat: popovers can only be non-modal and we want our menu to be modal in order to trap focus. The popover
attribute is baseline, however, so if we used that we would have proper browser support and get something workable.
But by Jingo, I want the menu to be modal! Is there no shiny API where we can tell our HTML what commands to execute?
Well, yes.
Introducing the Invoker Commands API:
The Invoker Commands API provides a way to declaratively assign behaviors to buttons, allowing control of interactive elements when the button is enacted (clicked or invoked via a keypress, such as the spacebar or return key).
As of writing this article the Invoker Commands API is still experimental and only supported on Chrome and Edge but support is growing fast (see below this article for current support). Until the API becomes final and baseline you can use the Invoker Buttons Polyfill to get a somewhat comparable functionality (the polyfill doesn't handle ARIA states, for one thing).
With the API we can now execute commands on others elements when a button is clicked by using the command
attribute. We want to open the dialog as a modal so we need to execute the showModal()
function. Also, we need to indicate on which element the command needs to be executed. For that we use the commandfor
attribute which takes the id
of the target element as a value.
Using the Invoker Commands API, our code now looks like this:
<button command="show-modal" commandfor="menu">Open menu</button>
<dialog id="menu">
<nav>
<ul>
<li>...</li>
</ul>
</nav>
<button command="close" commandfor="menu">Close menu</button>
</dialog>
And that's it. We now have a working mobile menu although it's a modal dialog that pops up in the center of the screen. Also, because the dialog is modal there is no light dismiss (closing the dialog by pressing the Esc key or clicking the backdrop). To get that, we would have to revert to a popover again. Then again, if we're being experimental…
<button command="show-modal" commandfor="menu">Open menu</button>
<dialog id="menu" closedby="any">
<nav>
<ul>
<li>...</li>
</ul>
</nav>
<button command="close" commandfor="menu">Close menu</button>
</dialog>
“But wait” I hear you say. “Does this mean we need an extra menu for desktop?” Not really; the dialog is just another HTML element and as most elements it can be put in “<div>
” mode (you know, like the opposite of the thing JavaScript bros do?):
@media (width >= 768px)
:is(#menu) {
position: static;
display: block;
opacity: 1;
color: currentColor;
background: transparent;
}
}
Granted, not the prettiest method but it works!