.TH new-menu 7 .SH NAME new-menu \- New Declarative Website Menu with Invoker Commands and noscript Hacks! .SH AUTHOR Artyom Bologov .SH SYNOPSIS 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! .SH TEXT .P So I read .UR https://dbushell.com/2026/02/12/declarative-dialog-menu-invoker-commands David Bushell’s post on Invoker Commands .UE 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 \fBprinciples (7)\fP important principal requirements for it: .P * It has to display an alternative full-length menu when JS is off or Invoker Commands are unsupported. .P * It should work on a multitude of screens. .P * It should be simple and HTML+CSS only. .P This post is a description of a technique I settled on. .SH noscript detection in CSS .P 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? .B Why “JS or Invoker Commands” .EX .P 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. .EE .P 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! .P .UR https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/noscript#usage_notes So noscript tag reference is quite quirky .UE : 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 \fBnoscript\fP. .P .in +4n .EX .EE .in .P — An element that only exists when JS is off .P And then check its presence in CSS with a \fB:root:has( #noscript)\fP pseudo-class. With that, the sky is the limit. We can, say, define a CSS class that only shows when JS is on: .P .in +4n .EX :root:has( #noscript) > .script { display: none; } .EE .in .P — The logic is inverted, but .script displays by default, so no need to spell the positive case out .SH Making HTML for the Menu and the Dialog .P 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: .P .in +4n .EX .EE .in .P — Fill in yours! .P (I leave the root link out of it, because it should be displayed at all times.) .P Then we can create the \fBdialog\fP that will be displayed. It has to be keyboard-accessible and easy enough to use. Thus the structure: .P .in +4n .EX
.EE .in .P — Note the form with close action—there has to be a way to close the dialog .P The ID of the \fBdialog\fP is useful in creating the menu button: .P .in +4n .EX .EE .in .P — Menu button using Invoker Commands .P This button toggles the \fBdialog\fP on whenever pressed. .SH Supporting CSS .P Now the tasty (and slightly frightening) stuff: CSS! First, we only decide to show the menu button on smaller screens: .P .in +4n .EX /* Adjust the width at your convenience */ @media (min-width: 500px) { #menu-button { display: none; } } .EE .in .P — We do not need the menu on larger screens .P Then, we have to deal with the fact that some browsers display the dialog unconditionally: .P .in +4n .EX dialog { /* ... */ display: none; } dialog:modal { display: block; } .EE .in .P — iOS, I swear I’ll… .P And, lastly, we have to hide the long list of links when on smaller screens and when Invoker Commands are likely on: .P .in +4n .EX /* Adjust the width at your convenience */ @media (max-width: 500px) { :root:not(:has( #noscript)) nav #nav-links { display: none; } } .EE .in .P — Do not show nav when JS is on and the screen is narrow .P This has a scary selector, true. But it’s actually .UR https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement progressively enhanced .UE ! 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! .SH Corner-cases and Problems .P There are three main problems with this approach: .P * 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. .P * 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. .P * Again, this approach relies on the heuristic “if JS is on, Invoker Commands are likely supported.” This is imperfect, but practical enough for me. .SH Show Me Your Menus! .P You can always send me an email via the feedback form below or \fBabout (7)\fP contact me elsewhere (section contacts). Care about your noJS and mobile users and be kind to people 💙 .SH COPYRIGHT .UR https://creativecommons.org/licenses/by/4.0 CC-BY 4.0 .UE 2022-2026 by Artyom Bologov (aartaka,) .UR https://codeberg.org/aartaka/pages/commit/a91befa with one commit remixing Claude-generated code .UE . Any and all opinions listed here are my own and not representative of my employers; future, past and present.