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.

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:
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:
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:
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:
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:
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?
Think it clicked? Take the 8-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
- Shadow DOM Needed JavaScript to Exist. Now It Doesn't.
- You built your modal with a
<div>and a focus trap library. The native<dialog>does all of that. - Stop Calling form.submit(). Use requestSubmit() Instead.
🚀 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:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
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.