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.
command
and commandfor
usage
Basic 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 ::backdrop
s 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>
show-popover
, hide-popover
, and toggle-popover
values
The <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.
popover
and <dialog>
the only features that support invoker commands?
Are 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.
There is an error in the link address to showModal(), in: “Specifically, itâs the HTML-invoked equivalent of JavaScriptâs showModal() method.”
Thanks for the heads-up!
… but the article itself is very interesting :) I learned something new again, thanks!