New Declarative Website Menu with Invoker Commands and noscript Hacks!

By Artyom Bologov A hand-drawn thumbnail. In the center, there’s a glyph for “hamburger” menu. From it, lines go in all directions. Lines say “hacky!” “noJS!” “HTML!” “CSS!” “declarative!” “dialog!”. Attributions to Artyom Bologov and aartaka.me are visible too.

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:

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>
An element that only exists when JS is off

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;
}
The logic is inverted, but .script displays by default, so no need to spell the positive case out

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>
Fill in yours!

(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>
Note the form with close action—there has to be a way to close the 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>
Menu button using Invoker Commands

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;
    }
}
We do not need the menu on larger screens

Then, we have to deal with the fact that some browsers display the dialog unconditionally:

dialog {
    /* ... */
    display: none;
}
dialog:modal {
    display: block;
}
iOS, I swear I’ll…

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;
    }
}
Do not show nav when JS is on and the screen is narrow

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:

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 💙

Leave feedback! (via email) #