Copy hi@ste․digital
Posted on

Levelling up your dialogs with [closedby] and command invokers

I recently watched Una Kravets' brilliant “What’s New in Web UI” talk at Google I/O, and wow — the pace at which new features are landing in CSS and HTML is wild. We’re talking customisable <select>s, native carousels, anchor positioning, and the focus of this post: some serious glow-ups for the humble <dialog> element.

In this article, I’m going to:

  • Recap what <dialog> is and when you should use it
  • Explore the common pain points (and how we used to solve them)
  • Introduce two game-changing additions: [closedby] and command invokers ([command] and [commandfor])
  • Show how these new features make modal dialogs more powerful and ergonomic

Let’s dive in.

What is <dialog> anyway?

The <dialog> element is a native HTML element designed for rendering modal dialogs. It’s been in the spec for a long time now, but until recently it had a bit of a reputation as the clunky cousin of JavaScript modals and third-party libraries. People have sometimes avoided it because it lacked some of the basic features developers expected out of the box.

Still, the foundation was solid:

  • You can open a dialog with .showModal()
  • It comes with a native backdrop and focus-trapping
  • Closing it via .close() triggers a close event, making lifecycle management easy

Why use <dialog>?

The <dialog> element offers a bunch of benefits:

  • Semantic HTML: It tells the browser (and assistive tech) what this thing is.
  • Accessibility: Focus is automatically trapped inside, and the rest of the page becomes inert (non-focusable)
  • No dependency: No need to reach for JavaScript libraries
  • Styleable backdrop included: The dialog element comes with its own overlay (can be targeted using ::backdrop)
  • Sits on top layer: it always renders on a new layer above the rest of the DOM tree

Here’s a typical usage example:

<dialog id="myModal">
  <p>My modal</p>

  <button onclick="document.getElementById('myModal').cloe()">Close</button>
</dialog>

<button onclick="document.getElementById('myModal').showModal()">Open Modal</button>

Where <dialog> used to fall short

Despite the <dialog> element's built-in accessibility and semantics, it still has some shortcomings out of the box as previously alluded to:

  • Light-dismiss (click outside) ❌
  • ESC key to close ✅
  • Open without JS ❌
  • Close without JS ❌
  • Declarative behavior ❌
  • Built-in backdrop ✅
  • Accessible focus trap ✅

This is where the new [closedby] attribute and command invokers come in, to turn those red crosses into green ticks!

Enter [closedby]

As of Chrome 134 (March 4th 2025), we now have a native way to define how a dialog should be dismissed by the user. The new closedby attribute can be used like this:

<dialog closedby="none"> <!-- can only be closed via JS -->
<dialog closedby="closerequest"> <!-- closes on ESC -->
<dialog closedby="any"> <!-- closes on ESC or clicking outside -->

This solves one of the biggest complaints about <dialog>: that it wasn’t light-dismissible. Now, with closedby="any", you can click outside the dialog to dismiss it - no JS required!

Command Invokers

As of Chrome 135 (April 1st 2025), we also have new declarative attributes: [command] and [commandfor]. These let you trigger actions (like opening/closing a dialog) directly from HTML:

<!-- Button invoking the command -->
<button command="show-modal" commandfor="my-dialog">Open Dialog</button>

<!-- Dialog defining the command -->
<dialog id="my-dialog">
  <button command="close" commandfor="my-dialog">Close</button>
</dialog>

No JS. Just HTML!

Obviously support for these is very limited having just shipped in Chrome, but there is a polyfill available.

Putting it all together

Let’s revisit our earlier dialog, but this time with all the new goodness:

<dialog id="my-dialog" closedby="any">
  <p>A light-dismiss-able, no JS modal 👌</p>
  <button command="close" commandfor="my-dialog">Close</button>
</dialog>

<button command="show-modal" commandfor="my-dialog">Open Modal</button>

That’s it. No JavaScript. Fully accessible. Light-dismissible. Declarative.

When to use <dialog> vs [popover]

If you’re not familiar with [popover], it’s another exciting new HTML attribute that allows lightweight UI overlays - things like tooltips, dropdowns, or cards - to behave natively without JS.

Use <dialog> when:

  • You want modal behavior (backdrop, focus trap, page inerting)
  • You’re asking the user to confirm, decide, or acknowledge something

Use [popover] when:

  • You want something light and transient (tooltips, cards, dropdowns)
  • You don’t need a backdrop or focus trap

I’ll be writing more about [popover] soon - it deserves its own spotlight.

Wrapping up

The addition of [closedby] and command invokers gracefully fills the functionality holes that I reckon have probably made a lot of people turn away from using the <dialog> element up to now.

No JavaScript required. Clear, declarative HTML. A11y baked in.

If you’re like me and have spent years wrestling with modals in JS (applying the appropriate ARIA attributes, moving the focus state, adding another digit to the z-index, etc. etc.) these new HTML-native options feel like a breath of fresh air!

And there's SO much more to come, too. We've come a long way since adding images for rounded corners!