Invoker Commands API
Most buttons on a page exist to do one thing: open something, close something, or turn something else on or off. Wiring that up has always meant writing a small script, one that finds the button, finds the target, and connects the two with a click listener. The Invoker Commands API lets the browser do that wiring itself, straight from the markup, for the two cases that come up constantly (opening a dialog, showing a popover) and for anything else through a single hook it hands you. Button, Fab, Modal, and Drawer all understand it already.
How it works
Two attributes turn a button into what the specification calls an
invoker. commandfor points at the element it controls, by ID.
command names what should happen to it. Clicking (or activating with
the keyboard) fires an event named command on the target element,
carrying which command was requested and which button asked for it. No
element reference, no addEventListener call, nothing to import.
The plain HTML below has no script attached to it at all, and no Eunomia
component either: a native <button> with command/commandfor,
pointed at a native <dialog>.
<button command="show-modal" commandfor="native-dialog-demo">
Open dialog
</button>
<dialog id="native-dialog-demo">
<p>...</p>
<button command="close" commandfor="native-dialog-demo">Close</button>
</dialog>
A handful of command values are built into the browser itself, so the target doesn't need any JavaScript of its own to respond to them:
| Command | Applies to | Does |
|---|---|---|
show-modal |
A <dialog>, or Eunomia's Modal and Drawer |
Opens it |
close |
A <dialog>, or Eunomia's Modal and Drawer |
Closes it immediately, without asking anything first |
request-close |
A <dialog>, or Eunomia's Modal and Drawer |
Asks it to close, respecting whatever confirmation the target requires before it actually does |
show-popover |
An element with the popover attribute |
Opens it |
hide-popover |
An element with the popover attribute |
Closes it |
toggle-popover |
An element with the popover attribute |
Opens it if closed, closes it if open |
A -- prefix |
Any target | Nothing built in. The target listens for command itself and decides what its own command name means |
That last row is the escape hatch: name a command --print,
--next-slide, anything your own application needs, and handle it by
listening for command on the target and checking event.command.
Every command event also carries event.source, the button that
triggered it, useful when several invokers share one target.
Using it with Eunomia
Button and Fab accept command/commandfor exactly like a native
button would. Modal and Drawer, as targets, understand show-modal,
close, and request-close, mapping each onto their own show(),
hide(), and requestClose() methods, so request-close still
respects whatever a given instance requires before it actually closes
(an unsaved-changes confirmation, for instance). The markup below is the
entire example: there's no <script> block because none is needed.
This will permanently remove the item from your workspace.
<eun-modal
id="delete-modal"
heading="Delete item"
confirm-label="Delete"
confirm-variant="critical"
>
<p>This will permanently remove the item from your workspace.</p>
</eun-modal>
<eun-button command="show-modal" commandfor="delete-modal"
>Delete item</eun-button
>
Reach for a custom command the same way you would on a native target,
by listening for command yourself:
<eun-button command="--print" commandfor="invoice">Print</eun-button>
document.getElementById("invoice").addEventListener("command", (event) => {
if (event.command === "--print") {
window.print();
}
});
Each component's own Examples tab has more: see
Why not just wire it up yourself
A click handler works fine for one button and one target. It stops
scaling once there are several: a delete button in a table row, a save
button in a form, a menu item that should each open their own dialog
means finding each element, keeping each reference around, and
attaching each listener by hand, all to do the exact same thing every
time. command/commandfor replaces that entire category of
boilerplate with two attributes, read once by the platform instead of
written out per case.
It also survives longer than a hand-written listener would. The two
attributes are parsed the moment the markup exists, so activation works
before any script has run, useful for anything server-rendered or
progressively enhanced. Keyboard activation (Enter, Space) comes
along for free too, since it rides on the same activation a button
already gets, rather than something you'd otherwise have to reimplement
on top of a click listener.
Why it's better than the Popover API alone
The Popover API on its own only ever covers one shape of target: an
element carrying a popover attribute, opened and closed through its
own popovertarget/popovertargetaction pair. A <dialog> needed an
entirely separate mechanism, showModal()/close() called from your
own script, since nothing declarative connected a button to it. Anyone
building both a popover and a dialog ended up reaching for two different
patterns to express the same idea: "this button opens that thing."
command/commandfor folds both into one mechanism. show-popover,
hide-popover, and toggle-popover cover exactly what
popovertargetaction used to, while show-modal, close, and
request-close bring dialogs into the same convention instead of
leaving them to plain script. Anything outside either category, a
carousel's next slide, a form step, an application-specific action,
still fits through a custom command, so there's one attribute pair and
one event to reach for regardless of what the target actually is.
Reach for command/commandfor by default for any new trigger, on a
Eunomia component or otherwise. popovertarget/popovertargetaction
still works wherever it's already in place, but it's the narrower,
older path now: everything it can do, command/commandfor also
does, plus dialogs and custom application behavior it was never built
for.
Browser support
The Invoker Commands API reached Baseline availability in December 2025,
shipped in Chrome/Edge 135, Firefox 144, and Safari 26.2. Button, Fab,
Modal, and Drawer don't wait on that rollout, though: each implements
command/commandfor itself in JavaScript rather than depending on the
browser to dispatch it, the same reason a custom element can't rely on
popovertarget working natively either, since only a real <button> is
recognized as an invoker by the browser. In practice, that means all
four work with command/commandfor on every browser Eunomia itself
supports, with nothing extra to install.
The one case that does need something extra is a plain native
<button command commandfor>, written outside any Eunomia component,
targeting one of these four on a browser old enough to lack native
support. See
See also
MDN: Invoker Commands API HTML Specification: the button command attribute Open UI: Invoker Commands explainer Modal andDrawer : the target side, in each component's own Examples tabButton andFab : the invoker sidePolyfills : what to install for a native invoker on an older browser