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.

The wiring you've written more times than you'd like to admit

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

<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> 
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.

The attributes that do the wiring for you

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:

<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.

🎮 Try it yourself

▶️ Open the interactive playground →

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.

It's the same three verbs for popovers

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:

<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.

The part that's actually new: commands you invent

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:

<button command="--mark-read" commandfor="notification-3">Mark as read</button> <li id="notification-3">New comment on your post</li> 
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.

The part worth being honest about

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:

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.

Eleven dialogs, zero listeners

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?

🧠 Test yourself

Think it clicked? Take the 8-question quiz →

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

📚 Read next

🚀 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: