I updated my website menu to be prettier on mobile, and I did not sacrifice accessibility and noJS folks! Go check it out and adopt it! assets/new-menu.png 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. IMAGE-ALT

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?

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:

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 💙