The Popover API and <dialog> element are two of my favorite new platform features. In fact, I recently [wrote a detailed overview of their use cases] and the sorts of things you can do with them, even learning a few tricks in the process that I couldn’t find documented anywhere else.
I’ll admit that one thing that I really dislike about popovers and dialogs is that they could’ve easily been combined into a single API. They cover different use cases (notably, dialogs are typically modal) but are quite similar in practice, and yet their implementations are different.
Well, web browsers are now experimenting with two HTML attributes — technically, they’re called “invoker commands” — that are designed to invoke popovers, dialogs, and further down the line, all kinds of actions without writing JavaScript. Although, if you do reach for JavaScript, the new attributes — command
and commandfor
— come with some new events that we can listen for.
Invoker commands? I’m sure you have questions, so let’s dive in.
We’re in experimental territory
Before we get into the weeds, we’re dealing with experimental features. To use invoker commands today in November 2024 you’ll need Chrome Canary 134+ with the enable-experimental-web-platform-features
flag set to Enabled
, Firefox Nightly 135+ with the dom.element.invokers.enabled
flag set to true
, or Safari Technology Preview with the InvokerAttributesEnabled
flag set to true
.
I’m optimistic we’ll get baseline coverage for command
and commandfor
in due time considering how nicely they abstract the kind of work that currently takes a hefty amount of scripting.
Basic command
and commandfor
usage
First, you’ll need a <button>
or a button-esque <input>
along the lines of <input type="button">
or <input type="reset">
. Next, tack on the command
attribute. The command
value should be the command name that you want the button to invoke (e.g., show-modal
). After that, drop the commandfor
attribute in there referencing the dialog or popover you’re targeting by its id
.
<button command="show-modal" commandfor="dialogA">Show dialogA</button>
<dialog id="dialogA">...</dialog>
In this example, I have a <button>
element with a command
attribute set to show-modal
and a commandfor
attribute set to dialogA
, which matches the id
of a <dialog>
element we’re targeting:
Let’s get into the possible values for these invoker commands and dissect what they’re doing.
Looking closer at the attribute values
The show-modal
value is the command that I just showed you in that last example. Specifically, it’s the HTML-invoked equivalent of JavaScript’s showModal()
method.
The main benefit is that show-modal
enables us to, well… show a modal without reaching directly for JavaScript. Yes, this is almost identical to how HTML-invoked popovers already work with thepopovertarget and popovertargetaction attributes, so it’s cool that the “balance is being redressed” as the Open UI explainer describes it, even more so because you can use the command
and commandfor
invoker commands for popovers too.
There isn’t a show
command to invoke show()
for creating non-modal dialogs. I’ve mentioned before that non-modal dialogs are redundant now that we have the Popover API, especially since popovers have ::backdrops and other dialog-like features. My bold prediction is that non-modal dialogs will be quietly phased out over time.
The close
command is the HTML-invoked equivalent of JavaScript’s close() method used for closing the dialog. You probably could have guessed that based on the name alone!
<dialog id="dialogA">
<!-- Close #dialogA -->
<button command="close" commandfor="dialogA">Close dialogA</button>
</dialog>
The show-popover
, hide-popover
, and toggle-popover
values
<button command="show-popover" commandfor="id">
…invokes showPopover()
, and is the same thing as:
<button popovertargetaction="show" popovertarget="id">
Similarly:
<button command="hide-popover" commandfor="id">
…invokes hidePopover()
, and is the same thing as:
<button popovertargetaction="hide" popovertarget="id">
Finally:
<button command="toggle-popover" commandfor="id">
…invokes togglePopover()
, and is the same thing as:
<button popovertargetaction="toggle" popovertarget="id">
<!-- or <button popovertarget="id">, since ‘toggle’ is the default action anyway. -->
I know all of this can be tough to organize in your mind’s eye, so perhaps a table will help tie things together:
command |
Invokes | popovertargetaction equivalent |
---|---|---|
show-popover |
showPopover() |
show |
hide-popover |
hidePopover() |
hide |
toggle-popover |
togglePopover() |
toggle |
So… yeah, popovers can already be invoked using HTML attributes, making command
and commandfor
not all that useful in this context. But like I said, invoker commands also come with some useful JavaScript stuff, so let’s dive into all of that.
Listening to commands with JavaScript
Invoker commands dispatch a command
event to the target whenever their source button is clicked on, which we can listen for and work with in JavaScript. This isn’t required for a <dialog>
element’s close
event, or a popover
attribute’s toggle
or beforetoggle
event, because we can already listen for those, right?
For example, the Dialog API doesn’t dispatch an event when a <dialog>
is shown. So, let’s use invoker commands to listen for the command
event instead, and then read event.command
to take the appropriate action.
// Select all dialogs
const dialogs = document.querySelectorAll("dialog");
// Loop all dialogs
dialogs.forEach(dialog => {
// Listen for close (as normal)
dialog.addEventListener("close", () => {
// Dialog was closed
});
// Listen for command
dialog.addEventListener("command", event => {
// If command is show-modal
if (event.command == "show-modal") {
// Dialog was shown (modally)
}
// Another way to listen for close
else if (event.command == "close") {
// Dialog was closed
}
});
});
So invoker commands give us additional ways to work with dialogs and popovers, and in some scenarios, they’ll be less verbose. In other scenarios though, they’ll be more verbose. Your approach should depend on what you need your dialogs and popovers to do.
For the sake of completeness, here’s an example for popovers, even though it’s largely the same:
// Select all popovers
const popovers = document.querySelectorAll("[popover]");
// Loop all popovers
popovers.forEach(popover => {
// Listen for command
popover.addEventListener("command", event => {
// If command is show-popover
if (event.command == "show-popover") {
// Popover was shown
}
// If command is hide-popover
else if (event.command == "hide-popover") {
// Popover was hidden
}
// If command is toggle-popover
else if (event.command == "toggle-popover") {
// Popover was toggled
}
});
});
Being able to listen for show-popover
and hide-popover
is useful as we otherwise have to write a sort of “if opened, do this, else do that” logic from within a toggle
or beforetoggle
event listener or toggle-popover
conditional. But <dialog>
elements? Yeah, those benefit more from the command
and commandfor
attributes than they do from this command
JavaScript event.
Another thing that’s available to us via JavaScript is event.source
, which is the button that invokes the popover
or <dialog>
:
if (event.command == "toggle-popover") {
// Toggle the invoker’s class
event.source.classList.toggle("active");
}
You can also set the command
and commandfor
attributes using JavaScript:
const button = document.querySelector("button");
const dialog = document.querySelector("dialog");
button.command = "show-modal";
button.commandForElement = dialog; /* Not dialog.id */
…which is only slightly less verbose than:
button.command = "show-modal";
button.setAttribute("commandfor", dialog.id);
Creating custom commands
The command
attribute also accepts custom commands prefixed with two dashes (--
). I suppose this makes them like CSS custom properties but for JavaScript events and event handler HTML attributes. The latter observation is maybe a bit (or definitely a lot) controversial since using event handler HTML attributes is considered bad practice. But let’s take a look at that anyway, shall we?
Custom commands look like this:
<button command="--spin-me-a-bit" commandfor="record">Spin me a bit</button>
<button command="--spin-me-a-lot" commandfor="record">Spin me a lot</button>
<button command="--spin-me-right-round" commandfor="record">Spin me right round</button>
const record = document.querySelector("#record");
record.addEventListener("command", event => {
if (event.command == "--spin-me-a-bit") {
record.style.rotate = "90deg";
} else if (event.command == "--spin-me-a-lot") {
record.style.rotate = "180deg";
} else if (event.command == "--spin-me-right-round") {
record.style.rotate = "360deg";
}
});
event.command
must match the string with the dashed (--
) prefix.
Are popover
and <dialog>
the only features that support invoker commands?
According to Open UI, invokers targeting additional elements such as <details>
were deferred from the initial release. I think this is because HTML-invoked dialogs and an API that unifies dialogs and popovers is a must-have, whereas other commands (even custom commands) feel more like a nice-to-have deal.
However, based on experimentation (I couldn’t help myself!) web browsers have actually implemented additional invokers to varying degrees. For example, <details>
commands work as expected whereas <select>
commands match event.command
(e.g., show-picker
) but fail to actually invoke the method (showPicker()
). I missed all of this at first because MDN only mentions dialog and popover.
Open UI also alludes to commands for <input type="file">
, <input type="number">
, <video>
, <audio>
, and fullscreen-related methods, but I don’t think that anything is certain at this point.
So, what would be the benefits of invoker commands?
Well, a whole lot less JavaScript for one, especially if more invoker commands are implemented over time. Additionally, we can listen for these commands almost as if they were JavaScript events. But if nothing else, invoker commands simply provide more ways to interact with APIs such as the Dialog and Popover APIs. In a nutshell, it seems like a lot of “dotting i’s” and “crossing-t’s” which is never a bad thing.