NEW DECLARATIVE WEBSITE MENU WITH INVOKER COMMANDS AND NOSCRIPT HACKS! (#📎) 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: • 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 (#noscript) 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'. =================================== html =================================== ==================== 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: =================================== css =================================== :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 (#html) 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: =================================== html =================================== ==================== 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: =================================== html ===================================
==================== 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: =================================== html =================================== ==================== Menu button using Invoker Commands ==================== This button toggles the `dialog' on whenever pressed. SUPPORTING CSS (#css) Now the tasty (and slightly frightening) stuff: CSS! First, we only decide to show the menu button on smaller screens: =================================== css =================================== /* 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: =================================== css =================================== 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: =================================== css =================================== /* 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 (#corner-cases) 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! (#show) 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 💙 CC-BY 4.0 2022-2026 by Artyom Bologov (aartaka,) with one commit remixing Claude-generated code . Any and all opinions listed here are my own and not representative of my employers; future, past and present.