Invoker Commands: Additional Ways To Work With Dialog, Popover… And More?


The Popover API and

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

...

In this example, I have a

The show-popover, hide-popover, and toggle-popover values

…invokes showPopover(), and is the same thing as:

Similarly:

…invokes hidePopover(), and is the same thing as:

Finally:

…invokes togglePopover(), and is the same thing as:

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

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

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

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

:

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:



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 the only features that support invoker commands?

According to Open UI, invokers targeting additional elements such as

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,

commands work as expected whereas , , , , 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.



Read more