A <button> carrying command and commandfor opens or closes a dialog or popover on its own, with no click handler anywhere. commandfor takes the id of the element being controlled, command names the action, and the browser handles activation, focus and the aria-expanded state. Both attributes have been Baseline since December 2025.
Dialogs and popovers are the two bits of UI that most often arrive with a click handler attached. The markup below needs none, so it works while the bundle is still downloading and it keeps working if the bundle fails.
What do command and commandfor do in HTML?
They turn a button into an invoker for another element. Nothing else on the page needs to know about it.
<button command="show-modal" commandfor="signup">Sign up</button>
<dialog id="signup">
<h2>Create an account</h2>
<form method="dialog">
<label>Email <input type="email" name="email" required></label>
<button value="confirm">Confirm</button>
</form>
<button command="close" commandfor="signup" value="cancel">Cancel</button>
</dialog>
Both attributes have to be present. A command with no commandfor points at nothing and does nothing, and a commandfor with no command has no action to run.
Only <button> gets this. The Open UI explainer is blunt about why: “invoking actions on non-button elements such as <div>s or <a>s creates many problems”. Submit and reset buttons are out as well.
You do not need type="button" to keep a command button from posting a form. The HTML specification makes a button with both command and commandfor a non-submit button in its auto state, so it is safe to drop one inside a <form>. Write type="submit" explicitly and you are back to a submit button with no command behaviour.
A disabled button runs nothing at all. The activation steps return before any of the command handling.
Which commands are built in?
Six keywords, and MDN documents each one as the declarative equivalent of a method call.
For <dialog>: show-modal is showModal(), close is close(), and request-close fires a cancel event followed by a close event, which gives your code a chance to intervene before the dialog goes. close has a detail worth knowing. If the button also has a value, that value becomes the dialog’s returnValue, so the cancel button in the sample above sets returnValue to "cancel" without a line of script.
For popovers: show-popover, hide-popover and toggle-popover. Showing an already-shown popover does nothing rather than throwing, and the same goes for hiding a hidden one.
Point a popover command at a <dialog>, or show-modal at a <div popover>, and you get silence. The spec returns early when the target does not support the command, so there is no built-in behaviour, no event, and nothing in the console. When a button appears dead, check that pairing first.
Other commands are still proposals. The Open UI explainer sketches show-picker for inputs and selects, playback control for media and a copy-to-clipboard action, and the Chrome team’s introduction to the attributes points at the same list. As of September 2026 the spec enumerates the six above and nothing more, so treat anything else you read about as unshipped.
How do custom commands work?
Prefix the value with two hyphens and the browser skips the built-in behaviour, dispatching a CommandEvent on the target instead. The prefix is mandatory, in the same spirit as CSS dashed idents.
<button command="--filter" commandfor="gallery" value="dresses">Dresses</button>
<button command="--filter" commandfor="gallery" value="coats">Coats</button>
const gallery = document.getElementById("gallery");
gallery.addEventListener("command", (event) => {
if (event.command !== "--filter") return;
gallery.dataset.filter = event.source.value;
for (const card of gallery.querySelectorAll("[data-category]")) {
card.hidden = card.dataset.category !== event.source.value;
}
});
Two properties matter on the event. command is the string from the attribute, and source is the HTMLButtonElement that invoked it, so the button’s own value can carry the argument and one listener serves a whole row of filters.
The listener goes on the target, not on the button, because that is where the event fires. The explainer describes these events as non-bubbling, so a delegated document.addEventListener("command", ...) never runs. If your first custom command appears to do nothing, check which element you bound the listener to before you start doubting the attribute value.
Built-in commands fire the same event before they act, and it is cancelable. Listening for show-modal and calling preventDefault() when a form is half-finished keeps the dialog shut, with the button markup untouched.
Should you rip out popovertarget?
Not urgently. popovertarget and popovertargetaction still work, and MDN describes the newer pair as “very similar functionality … but with a more general design aimed at providing other functionality beyond popover commands, including custom commands”. If you have popovertarget buttons shipping today, they are not broken.
New work is where the switch pays. One attribute pair covers dialogs, popovers and your own actions, which leaves a component library with a single pattern to teach instead of two. Our comparison of the Popover API and dialog covers which of the two elements you actually want underneath.
Do not put both attribute pairs on one button. Pick the one you are standardising on and keep it consistent per component.
The accessibility mapping comes free, which a hand-rolled toggle does not. Per the explainer, a button that is not a descendant of its target picks up an implicit aria-expanded reflecting whether the target is open. A button nested inside the target gets no expanded state at all, which is right: a close button inside a dialog is not a disclosure control. Write the same thing yourself and you own aria-expanded in the open path, the close path, and the Escape-key path, which is three places for it to go stale.
Is it safe to ship in 2026?
Invoker commands went Baseline newly available on 12 December 2025, when Safari 26.2 landed the feature. Chrome and Edge 135 shipped it on 1 April 2025, Firefox 144 on 14 October 2025. The web features explorer puts widely available in June 2028, so for the next couple of years you are supporting browsers that predate it.
The fallback is small, because each built-in command is the declarative form of a method call you would otherwise have written.
if (!("command" in HTMLButtonElement.prototype)) {
document.addEventListener("click", (event) => {
const button = event.target.closest("button[commandfor]");
if (!button) return;
const target = document.getElementById(button.getAttribute("commandfor"));
if (!target) return;
switch (button.getAttribute("command")) {
case "show-modal": target.showModal(); break;
case "close": target.close(button.value); break;
case "request-close":
target.requestClose ? target.requestClose() : target.close();
break;
case "show-popover": target.showPopover(); break;
case "hide-popover": target.hidePopover(); break;
case "toggle-popover": target.togglePopover(); break;
}
});
}
The detection works because both attributes reflect as HTMLButtonElement.command and HTMLButtonElement.commandForElement. Custom commands are outside this shim; supporting those in old browsers means constructing and dispatching your own event, and at that point you may as well keep the handler.
Use the built-in commands now, behind that shim, for any dialog or popover you are writing this year. Use custom commands when the alternative is a delegated click handler that reads data attributes off the button, which they replace cleanly. Skip them where your framework already owns the open state, since a React or Svelte component that re-renders from its own signal will fight a browser that changes the dialog underneath it. The progressive enhancement argument for server-rendered forms applies here as well: markup that works before the bundle arrives is worth more than markup that is slightly tidier.
We build front ends where the interactive parts still work when JavaScript is slow or absent, which tends to be the same work as making them usable with a keyboard and a screen reader. If that side of a build needs attention, our accessibility and WCAG compliance work is where it lives.