Invoker Commands: HTML Attributes That Trigger Dialogs and Popovers
The <dialog> element and the Popover API are both powerful platform features, but they’ve always felt like two halves of a whole. Their implementations differ enough that working with both means juggling separate APIs and a fair amount of JavaScript. Browsers are now experimenting with a pair of attributes — command and commandfor — that aim to unify and simplify how we trigger these UI components.
Experimental Status and How to Test
As of November 2024, these “invoker commands” are only available in pre-release browsers. You’ll need Chrome Canary 134+ with the enable-experimental-web-platform-features flag enabled, Firefox Nightly 135+ with dom.element.invokers.enabled set to true, or Safari Technology Preview with the InvokerAttributesEnabled flag toggled on.
How the Attributes Work
The system is straightforward: add the command attribute to a <button> or a button-like <input> element, then point the commandfor attribute at the target dialog or popover’s id. The command value specifies the action to perform.
<button command="show-modal" commandfor="dialogA">Show dialogA</button>
<dialog id="dialogA">...</dialog>
In the above example, a button with command="show-modal" and commandfor="dialogA" opens the dialog whose id matches dialogA.
Built-In Command Values
The available command values map directly to existing JavaScript methods:
show-modal— the HTML equivalent ofshowModal()for dialogs, offering a script-free way to display a modal.close— the counterpart to JavaScript’sclose()method for dismissing dialogs.show-popover— invokes theshowPopover()method.hide-popover— invokeshidePopover().toggle-popover— invokestogglePopover().
Notably, there is no show command for non-modal dialogs. Since popovers now offer similar functionality with added features like backdrop styling, the thinking is that non-modal dialogs will eventually become redundant.
Since popovers already work with HTML attributes like popovertarget and popovertargetaction, the command/commandfor pair doesn’t add much here. The real value for popovers lies in the associated JavaScript events, discussed below. For dialogs, though, these attributes fill a real gap, mirroring what already existed for popovers.
Listening for Command Events
Beyond the declarative syntax, invoker commands dispatch a command event to the target element when the source button is clicked. This is particularly useful for dialogs, since the Dialog API doesn’t fire an event when a dialog is shown. The event.command property reveals which action was triggered:
// 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
}
});
});
The same pattern applies to popovers. Having dedicated show-popover and hide-popover events is a cleaner alternative to toggling logic inside a single toggle or beforetoggle event listener.
// 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
}
});
});
The event.source property provides a reference back to the invoking button:
if (event.command == "toggle-popover") {
// Toggle the invoker’s class
event.source.classList.toggle("active");
}
The attributes can also be set programmatically:
const button = document.querySelector("button");
const dialog = document.querySelector("dialog");
button.command = "show-modal";
button.commandForElement = dialog; /* Not dialog.id */
Creating Custom Commands
Beyond the built-in values, the command attribute accepts custom commands prefixed with two dashes (--), similar in spirit to CSS custom properties. The event.command value must match the prefixed string exactly.
<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";
}
});
Beyond Dialogs and Popovers
The Open UI specification initially deferred support for other elements like <details>. In practice, however, experimentation shows browsers have implemented additional invokers to varying degrees. <details> commands appear to work as expected, while commands for <select> elements match on event.command but don’t actually invoke the corresponding method like showPicker(). Open UI also hints at future commands for file inputs, number inputs, media elements, and fullscreen methods, though nothing is finalized.
The Bottom Line
The primary benefit is less JavaScript, particularly if the invoker surface expands beyond dialogs and popovers over time. Even as it stands now, the ability to listen for command events gives us more flexible hooks into how we interact with these APIs. Invoker commands won’t necessarily replace existing patterns in every case — in some scenarios they’re less verbose, in others more so — but they offer useful alternatives for common UI actions.



