← blog
HTMLNovember 8, 2026 · 5 min read

Stop Writing JavaScript to Open Dialogs. commandfor Fixes It.

Every <dialog> and popover in your app still needs an addEventListener just to open and close — commandfor and the Invoker Commands API let the browser do it with zero JavaScript.

Parsa Jiravand · Frontend engineer · building bestpractic
Stop Writing JavaScript to Open Dialogs. `commandfor` Fixes It.

Open your project and search for .showModal(). Now search for addEventListener next to a close button. If you've built more than two dialogs, you've probably written the same eight lines more than twice — get the dialog, open it, find the button inside it, close it.

None of those eight lines are your app's logic. They're you, manually, telling the browser what it already knows how to do.

Here's the pattern. It's not bad code — it's just the same code, over and over:

HTML
1
2
3
4
5
<button id="open-btn">Delete account</button> <dialog id="confirm-dialog"> <p>This can't be undone. Are you sure?</p> <button id="close-btn">Cancel</button> </dialog>
JavaScript
1
2
3
4
5
6
7
const dialog = document.getElementById("confirm-dialog"); document.getElementById("open-btn").addEventListener("click", () => { dialog.showModal(); }); document.getElementById("close-btn").addEventListener("click", () => { dialog.close(); });

Every new dialog means another pair of getElementById calls and another pair of listeners. Multiply that by every confirm box, settings panel, and "are you sure" prompt in a real app, and you've got a module's worth of code whose entire job is opening and closing boxes.

Quick question before you scroll: if popovers already have a zero-JS way to open and close — popovertarget — why don't dialogs?

They do now.

command and commandfor are two new HTML attributes you put on a <button>. commandfor names the id of the element to act on; command names what to do to it:

HTML
1
2
3
4
5
6
<button command="show-modal" commandfor="confirm-dialog">Delete account</button> <dialog id="confirm-dialog"> <p>This can't be undone. Are you sure?</p> <button command="close" commandfor="confirm-dialog">Cancel</button> </dialog>

Delete the <script> tag. It isn't there anymore. The browser reads command="show-modal", finds the element with id="confirm-dialog", and calls the equivalent of .showModal() on it when the button is clicked — and command="close" does the equivalent of .close(). No listeners, no element lookups, no JavaScript file for a dialog that doesn't need any application logic at all.

Runs right in your browser — poke at it and watch the concept react live.

If you've used <dialog> before, you already know the third piece this unlocks: request-close. It's the declarative version of dialog.requestClose() — it asks the dialog to close the same way pressing Escape does, which means a cancel listener on the dialog still gets a chance to intervene (to warn about unsaved changes, say) before it actually closes. close just closes it, no questions asked.

commandfor isn't a dialog-only trick — it's the general-purpose version of popovertarget, and the command names map directly onto the popovertargetaction values you might already know:

HTML
1
2
3
4
<button popovertarget="old-way">The popovertarget way (still works)</button> <button command="toggle-popover" commandfor="new-way">The commandfor way</button> <div id="new-way" popover>Same result, same attribute family as dialogs now.</div>

show-popover, hide-popover, and toggle-popover do exactly what they say. The target still needs its own popover attribute either way — commandfor just gives you one consistent vocabulary (command/commandfor) instead of a dialog-shaped API and a separate popover-shaped API that happen to do similar things.

Built-in commands cover dialogs and popovers, but command also accepts custom values — anything prefixed with --. Point one at any element, and that element gets a command event instead of a built-in action:

HTML
1
2
<button command="--mark-read" commandfor="notification-3">Mark as read</button> <li id="notification-3">New comment on your post</li>
JavaScript
1
2
3
4
5
6
document.getElementById("notification-3").addEventListener("command", (event) => { if (event.command === "--mark-read") { event.source.disabled = true; // the button that triggered it event.target.classList.add("read"); // the element it was pointed at } });

The event fires on the target (the element named by commandfor), not on the button — which is the opposite of where a plain click listener usually lives, and worth double-checking the first time something doesn't fire where you expected. event.command tells you which command string triggered it, and event.source hands you back the exact button that was clicked, so one listener on a list can handle "mark read" buttons scattered across a dozen list items without you wiring each one individually.

For the built-in commands, that same event is cancelable: listen for command on the dialog, call event.preventDefault(), and you can override or block show-modal/close instead of just accepting the default.

command/commandfor reached Baseline "newly available" across all three engines in December 2025 — Chrome and Edge 135 (April 2025), Firefox 144 (October 2025), and Safari 26.2 (December 2025). That's real cross-browser support, not a single-vendor experiment like some of the APIs that showed up here first. But "newly available" means some of your readers are still one browser update behind, so feature-detect before you drop the fallback entirely:

JavaScript
1
2
3
4
const supportsCommands = "command" in HTMLButtonElement.prototype; if (!supportsCommands) { // keep your old getElementById + addEventListener path for now }

One habit worth keeping regardless of support: give these buttons an explicit type="button". A <button> inside a <form> defaults to submitting it, and command/commandfor don't change that default the way you might assume — so a "Delete account" button that's supposed to open a confirmation dialog can also quietly submit the form it's sitting in if you don't pin the type down yourself.

Go back to that side project with eleven dialogs. Every getElementById-showModal-addEventListener-close block becomes two attributes on two buttons, and the JavaScript file that used to exist purely to wire them up stops needing to exist at all. The logic that's left is the logic that was always actually yours — what happens after the dialog closes, not how it opened.

You already stopped writing JavaScript to open popovers once popovertarget showed up. Now dialogs get the same deal, plus a command event that's general enough to wire up things neither built-in verb ever covered. What's the oldest "open this, close that" wiring code still sitting in your codebase — would commandfor delete it today, or are you stuck waiting on a browser version?

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.


🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.

Thanks for reading! Let's stay connected:

Keep reading

One post a day, in your inbox

Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.

0 comments

Sign in to join the discussion, like comments, and save articles for later.