This content originally appeared on dbushell.com (blog) and was authored by dbushell.com (blog)
The off-canvas menu — aka the Hamburger, if you must — has been hot ever since Jobs’ invented mobile web and Ethan Marcott put a name to responsive design.
My journey
Making an off-canvas menu free from heinous JavaScript has always been possible, but not ideal. I wrote up one technique for Smashing Magazine in 2013. Later I explored <dialog> in an absurdly titled post where I used the new
Current thoughts
I strongly push clients towards a simple, always visible, flex-box-wrapping list of links. Not least because leaving the subject unattended leads to a multi-level monstrosity.
I also believe that good design and content strategy should allow users to navigate and complete primary goals without touching the “main menu”. However, I concede that Hamburgers are now mainstream UI. Jason Bradberry makes a compelling case.
My new menu
This month I redesigned my website. Taking the menu off-canvas at all breakpoints was a painful decision. I’m still not at peace with it. I don’t like plain icons. To somewhat appease my anguish I added big bold “Menu” text.
The HTML for the button is pure declarative goodness.
<button type="button" commandfor="menu" command="show-modal">
<span class="visually-hidden">open</span> Menu
</button>Accessibility updates: I originally added the extra “open” for clarity. It was noted that prefixes can cause issues for voice control and that my addition is unnecessary anyway. I removed that from my live site. It was also noted there was no navigation landmark on the page. This can be solved by wrapping the <button> in a <nav> element, which I have now done. Thanks for the feedback!
Aside note: Ana Tudor asked do we still need all those “visually hidden” styles? I’m using them out of an abundance of caution but my feeling is that Ana is on to something.
The menu HTML is just as clean.
<dialog id="menu">
<h2 class="hidden">Menu</h2>
<button type="button" commandfor="menu" command="close">
Close <span class="visually-hidden">menu</span>
</button>
<nav>
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/services/">Services</a></li>
<li><a href="/about/">About</a></li>
<li><a href="/blog/">Blog</a></li>
<li><a href="/notes/">Notes</a></li>
<li><a href="/contact/">Contact</a></li>
</ul>
</nav>
</dialog>It’s that simple! I’ve only removed my opinionated class names I use to draw the rest of the owl. I’ll explain more of my style choices later.
This technique uses the wonderful new popover I mentioned earlier. With a real <dialog> we get free focus management and more, as Chris Coyier explains. I made a basic CodePen demo for the code above.
The JavaScript
So here’s the bad news. Invoker commands are so new they must be polyfilled for old browsers. Good news; you don’t need a hefty script. Feature detection isn’t strictly necessary.
const $menu = document.querySelector("#menu");
for (const $button of document.querySelectorAll('[commandfor="menu"]')) {
$button.addEventListener("click", (ev) => {
ev.preventDefault();
if ($menu.open) $menu.close();
else $menu.showModal();
});
}Keith Cirkel has a more extensive polyfill if you need full API coverage like JavaScript events. My basic version overrides the declarative API with the JavaScript API for one specific use case, and the behaviour remains the same.
WebKit focus, visible?
Let’s get into CSS by starting with my favourite:
:focus-visible {
outline: 2px solid magenta;
outline-offset: 2px;
}A strong contrast outline around buttons and links with room to breath. This is not typically visible for pointer events. For other interactions like keyboard navigation it’s visible.
The first button inside the dialog, i.e. “Close (menu)”, is naturally given focus by the browser (focus is ‘trapped’ inside the dialog). In most browsers focus remains invisible for pointer events. WebKit has bug. When using showModal or invoker commands the focus-visible style is visible on the close button for pointer events. This seems wrong, it’s inconsistent, and clients absolutely rage at seeing “ugly” focus — seriously, what is their problem?!
I think I’ve found a reliable ‘fix’. Please do not copy this untested. From my limited testing with Apple devices and macOS VoiceOver I found no adverse effects. Below I’ve expanded the ‘not open’ condition within the event listener.
if ($menu.open) {
$menu.close();
} else {
$menu.showModal();
if (ev.pointerId > 0) {
const $active = document.activeElement;
if ($active.matches(":focus-visible")) {
$active.blur();
$active.focus({ focusVisible: false });
}
}
}First I confirm the event is relevant. I can’t check for an instance of PointerEvent because of the click handler. I’d have to listen for keyboard events and that gets murky. Then I check if the focused element has the visible style. If both conditions are true, I remove and reapply focus in a non-visible manner. The focusVisible boolean is Safari 18.4 onwards.
Like I said: extreme caution! But I believe this fixes WebKit’s inconsistency. Feedback is very welcome. I’ll update here if concerns are raised.
Click to dimiss
Native dialog elements allow us to press the ESC key to dismiss them. What about clicking the backdrop? We must opt-in to this behaviour with the closedby="any" attribute. Chris Ferdinandi has written about this and the JavaScript fallback.
That’s enough JavaScript!
Fancy styles
My menu uses a combination of both basic CSS transitions and cross-document
#menu {
opacity: 0;
transition:
opacity 300ms,
display 300ms allow-discrete,
overlay 300ms allow-discrete;
&[open] {
opacity: 1;
}
}
@starting-style {
#menu[open] {
opacity: 0;
}
}As an example here I fade opacity in and out. How you choose to use nesting selectors and the @starting-style rule is a matter of taste. I like my at-rules top level.
My menu also transitions out when a link is clicked. This does not trigger the closing dialog event. Instead the closing transition is mirrored by a cross-document view transition.
The example below handles the fade out for page transitions.
@view-transition {
navigation: auto;
}
#menu {
view-transition-name: --menu;
}
@keyframes --menu-old {
from { opacity: 1; }
to { opacity: 0; }
}
::view-transition-old(--menu) {
animation: --menu-old 300ms ease-out forwards;
}Note that I only transition the old view state for the closing menu. The new state is hidden (“off-canvas”). Technically it should be possible to use view transitions to achieve the on-page open and close effects too. I’ve personally found browsers to still be a little janky around view transitions — bugs, or skill issue?
It’s probably best to wrap a media query around transitions.
@media not (prefers-reduced-motion: reduce) {
/* fancy pants transitions */
}“Reduced” is a significant word. It does not mean “no motion”. That said, I have no idea how to assess what is adequately reduced! No motion is a safe bet… I think?
So there we have it! Declarative dialog menu with invoker commands, topped with a medley of CSS transitions and a sprinkle of almost optional JavaScript. Aren’t modern web standards wonderful, when they work?
I can’t end this topic without mentioning Jim Nielsen’s menu. I won’t spoil the fun, take a look! When I realised how it works, my first reaction was “is that allowed?!” It work’s remarkably well for Jim’s blog. I don’t recall seeing that idea in the wild elsewhere.
Thanks for reading! Follow me on Mastodon and Bluesky. Subscribe to my Blog and Notes or Combined feeds.
This content originally appeared on dbushell.com (blog) and was authored by dbushell.com (blog)
dbushell.com (blog) | Sciencx (2026-02-12T15:00:00+00:00) Declarative Dialog Menu with Invoker Commands. Retrieved from https://www.scien.cx/2026/02/12/declarative-dialog-menu-with-invoker-commands/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.