Design · · 6 min read
The dialog you can open, close and style without JavaScript
A button attribute opens a modal and a pseudo-class styles it, with no script. What's Baseline, what still needs JavaScript, and the attribute Safari hasn't shipped.
A modal dialog with a trigger, a close button and open-state styling, with no JavaScript at all:
<button command="show-modal" commandfor="signup">Sign up</button>
<dialog id="signup">
<h2>Create an account</h2>
<form method="dialog">
<input type="email" name="email" required>
<button value="accept">Continue</button>
</form>
<button command="close" commandfor="signup">Cancel</button>
</dialog>dialog::backdrop { background: rgb(0 0 0 / 0.5); }
#signup:open { animation: pop 150ms ease-out; }The command and commandfor attributes became Baseline newly available on 12 December 2025, and the :open pseudo-class on 11 May 2026. One piece of the picture — light dismiss via the closedby attribute — is not cross-browser yet, and that's the part to read carefully before you delete anything.
Opening and closing with buttons
commandfor takes the id of an element in the same tree; command says what to do to it. The HTML Standard defines six keywords, plus a custom form:
command value | Effect |
|---|---|
show-modal | Opens the targeted <dialog> as modal |
close | Closes the targeted <dialog> |
request-close | Requests to close it — fires cancel first, which can be prevented |
show-popover | Shows the targeted popover |
hide-popover | Hides it |
toggle-popover | Shows or hides it |
a keyword starting with -- | Dispatches a command event on the target and nothing else |
Two absences are worth noticing. There is no keyword for opening a non-modal dialog: show-modal is the only opening command, so a declaratively-opened dialog is always modal. For a non-modal overlay with no script, use the popover attribute instead — show-popover, hide-popover and toggle-popover are there for exactly that.
And an unrecognised value is not an error you'll see. The attribute's missing and invalid value defaults are both an "Unknown" state, so a typo like command="showmodal" silently does nothing.
Why request-close exists
close closes the dialog immediately. request-close behaves as if the user had asked to close it: a cancel event fires first, and if a listener calls preventDefault(), the dialog stays open. That's the hook for "you have unsaved changes" — the only part of that flow needing JavaScript is the condition itself.
The matching DOM method, requestClose(), ignores the closedby attribute entirely: even with closedby="none", calling it closes the dialog.
Custom commands
A command value starting with two hyphens does nothing except dispatch a command event on the target element, where event.source is the button that fired it. It's a declarative way to wire a button to a component without an id-based event listener of your own — useful in component libraries, and available in the same browser versions as the built-in keywords.
Styling the open state
:open matches any element that has open and closed states and is currently open — the spec names <details>, <select>, <dialog> and <input>.
details:open summary { border-bottom-color: transparent; }
select:open { outline: 2px solid Highlight; } /* while the picker shows */
dialog:open { translate: 0 0; }For <details> and <dialog> you could already select the open attribute with [open]. The reason :open matters is the elements with no attribute to select: a <select> showing its picker has no markup change at all, so before :open there was no way to style it from CSS.
Three details to keep straight:
:openis a semantic state, not a visibility state. The spec is explicit that an element inside adisplay: nonesubtree, or withvisibility: collapse, still matches:openif it is semantically open.:popover-openis a different thing. An element can be open in its own right and be a showing popover; the two pseudo-classes are deliberately separate. Use:popover-openfor popovers.- There is no
:closed. The spec notes one might be added "once the full set of things that support:openis known". Until then, negate::not(:open).
Support for <input> pickers is the shakiest corner — the web-platform test for it is marked optional — so treat input:open as a nice-to-have rather than a mechanism.
Because :open is a plain selector, :has() gives you page-level reactions without a class or a script:
/* fade a sticky header while any dialog is open */
body:has(dialog:open) .site-header { opacity: 0.4; }The gap: light dismiss
A modal opened with show-modal already closes on Escape, because the dialog's close-behaviour defaults to treating a close request as a close when it was shown modally. What it doesn't do by default is close when you click outside it.
That's what closedby is for:
closedby | Closes on |
|---|---|
any | Escape and a click outside the dialog |
closerequest | Escape only |
none | Neither — only close(), requestClose() or a close command |
| omitted | The Auto state: behaves as closerequest when opened with showModal(), otherwise as none |
And here is the honest support position at the time of writing: closedby shipped in Chrome and Edge 134 (March 2025) and Firefox 141 (July 2025), is in Safari Technology Preview, and is not available in Safari on iOS. Since it's an unknown attribute in Safari, it's simply ignored — a closedby="any" dialog still opens and still closes on Escape, it just won't light-dismiss.
That degradation is mild enough to ship as progressive enhancement for most dialogs. If click-outside dismissal is load-bearing for your interface, that's the one place you still need a few lines of script, or a popover instead — popover="auto" has light dismiss everywhere it's supported.
What still needs JavaScript
Declarative dialogs cover opening, closing and styling. They don't cover:
- Reading the result.
<form method="dialog">closes the dialog on submit and setsreturnValuefrom the submitter'svalue, with no script — but readingdialog.returnValueafterwards is JavaScript. - Conditional closing. The
cancellistener behindrequest-close. - Content loaded on demand. Nothing here fetches anything.
- Anything the user's data decides. Which dialog, which copy, which state.
What you get back is the fiddly part: the top layer, the backdrop, focus moving into the dialog and returning on close, and Escape handling all come from showModal(), which the show-modal command calls for you.
Browser support
| Feature | Chrome, Edge | Firefox | Safari | Baseline |
|---|---|---|---|---|
command, commandfor, CommandEvent | 135 (April 2025) | 144 (14 October 2025) | 26.2 (12 December 2025) | Newly available, 12 December 2025 |
:open | 133 (February 2025) | 136 (4 March 2025) | 26.5 (11 May 2026) | Newly available, 11 May 2026 |
<dialog closedby> | 134 (March 2025) | 141 (22 July 2025) | Technology Preview; no iOS | Not Baseline |
<dialog> itself | 37, Edge 79 | 98 | 15.4 | Widely available since 14 September 2024 |
Support data from @mdn/browser-compat-data 8.1.1 and web-features 3.38.0, correct at the time of writing. Note that Chrome and Edge recognised :open from version 114 without it having any effect — a partial implementation, so @supports selector(:open) is not a reliable test in that range.
Dialogs and popovers are a focus area of Interop 2026, which covers closedby, :open and popover="hint" specifically.
FAQ
Can I open a non-modal dialog without JavaScript?
No. show-modal is the only command that opens a dialog, and it opens it modally. For a non-modal overlay, use the popover attribute with show-popover or toggle-popover.
Does command work on any element?
The attributes are defined on <button>. A button that is inside a <select> element doesn't act as an invoker.
What happens in a browser that doesn't support command?
The button does nothing. If the dialog matters, render the page so that the content is still reachable — or add a click handler as a fallback and let the declarative path take over where it works.
Is :open the same as the open attribute?
Not quite. [open] selects markup, which only exists on <details> and <dialog>. :open selects the semantic state, which also covers a <select> whose picker is showing.
Sources
- HTML Standard — the
commandattribute — keywords, states and custom command keywords - HTML Standard —
<dialog closedby>— the three keywords and the Auto state - HTML Standard —
dialog.requestClose()— the cancel-then-close behaviour - Selectors Level 4 — the
:openpseudo-class — CSS Working Group Editor's Draft @mdn/browser-compat-data— version, release-date and partial-implementation data- Interop 2026 focus areas — dialogs and popovers focus area
More to read
- Tools
Tools · · 6 min read
Interop 2026: what the browser makers agreed to fix this year
Twenty focus areas and four investigations, chosen by the people who ship the engines. What Interop tells you that Baseline can't, and how to read the dashboard.
- Tech
Tech · · 7 min read
The first day of the week, without a lookup table
Intl.Locale's info methods are Baseline as of July 2026. What getWeekInfo(), getTextInfo() and getCalendars() return, with real values across Asia-Pacific locales.