New Declarative Website Menu with Invoker Commands and noscript Hacks!
By Artyom Bologov
So I read David Bushell’s post on Invoker Commands and it got me inspired! (In short: Invoker Commands API is a way to send events and open dialogs without JS.) I wanted to make a menu like that for my own site. However, I had important principal requirements for it:
- It has to display an alternative full-length menu when JS is off or Invoker Commands are unsupported.
- It should work on a multitude of screens.
- It should be simple and HTML+CSS only.
This post is a description of a technique I settled on.
noscript detection in CSS #
So a question I asked David when I planned for this post was quite hard. How do I detect if JS or Invoker Commands are on, using pure CSS?
Why “JS or Invoker Commands”
This sure has some minor corner-cases. But the heuristic is: if it supports JS, then it’s one of the major browsers. And if it’s one of the major browsers, it does support Invoker Commands too.
David did not have a solution, and I don’t blame him. It’s a non-goal for him to make the website fully accessible to noJS people. But my goal is!
So noscript tag reference is quite quirky:
it interprets its content as HTML when JS is off.
And when JS is on, it interprets it as… plaintext.
Which is really weird, but it actually opens a way for detection of noscript.
In short, you have to add an easily identifiable element inside noscript.
<noscript>
<span id=noscript></span>
</noscript>
And then check its presence in CSS with a :root:has( #noscript) pseudo-class.
With that, the sky is the limit.
We can, say, define a CSS class that only shows when JS is on:
:root:has( #noscript) > .script {
display: none;
}
Making HTML for the Menu and the Dialog #
Now that we have a way to detect noJS browsers, we can get to writing our HTML and CSS. First, the easy part: creating the long menu:
<nav>
<a href="/">Artyom’s Chaotic Blog</a>
<span id=nav-links>
<a href="/rss.xml">(RSS)</a>
<a href="/about.html">About & Contacts</a>
<!-- ... -->
</span>
</nav>
(I leave the root link out of it, because it should be displayed at all times.)
Then we can create the dialog that will be displayed.
It has to be keyboard-accessible and easy enough to use.
Thus the structure:
<dialog id=menu-dialog closedby=any>
<form formmethod="dialog">
<button>Close menu</button>
</form>
<ul>
<li> <a href="/">Artyom’s Chaotic Blog</a>
<li> <a href="/rss.xml">(RSS)</a>
<-- ... -->
</ul>
</dialog>
The ID of the dialog is useful in creating the menu button:
<nav>
<!-- Accessibility essential skiplink, include yours too! -->
<a class=skiplink href=#maincontent>Skip to content</a>
<a href="/">Artyom’s Chaotic Blog</a>
<!-- ... -->
<button id=menu-button commandfor=menu-dialog command=show-modal>Menu</button>
</nav>
This button toggles the dialog on whenever pressed.
Supporting CSS #
Now the tasty (and slightly frightening) stuff: CSS! First, we only decide to show the menu button on smaller screens:
/* Adjust the width at your convenience */
@media (min-width: 500px) {
#menu-button {
display: none;
}
}
Then, we have to deal with the fact that some browsers display the dialog unconditionally:
dialog {
/* ... */
display: none;
}
dialog:modal {
display: block;
}
And, lastly, we have to hide the long list of links when on smaller screens and when Invoker Commands are likely on:
/* Adjust the width at your convenience */
@media (max-width: 500px) {
:root:not(:has( #noscript)) nav #nav-links {
display: none;
}
}
This has a scary selector, true. But it’s actually progressively enhanced! Whenever these pseudo-classes are unsupported, links simply stay displayed. So noJS browsers and older browsers get this list of links. That’s basically it!
Corner-cases and Problems #
There are three main problems with this approach:
- The menu button stays visible for all small screens at all times. Given that there’s no way to detect Invoker Commands support in CSS, I cannot hide it when unsupported. This is, however, only a problem for browsers that don’t have Invoker Commands support.
- Invoker Commands API is not supported in iOS 18 (which I use,) only in iOS 26. So my own phone lacks navigation when JS is on (it usually isn’t.) This should not be a problem, as most mobile browsers nowadays have support for Invoker Commands. And browsers that don’t—they likely don’t support JavaScript, which means fallback link list is displayed there.
- Again, this approach relies on the heuristic “if JS is on, Invoker Commands are likely supported.” This is imperfect, but practical enough for me.
Show Me Your Menus! #
You can always send me an email via the feedback form below or contact me elsewhere. Care about your noJS and mobile users and be kind to people 💙