Components
Dialog
A minimally styled window overlaid on the main content. By design the Dialog is minimal with zero content to allow for both modal and non-modal use.
Full support Supported since v135. Partial support
Missing:
container-style-queries, overlay.
Partial support
Missing: dialog-closedby, overlay.
Modal vs Dialog
The term "modal" and "dialog" are often used interchangeably, but there's an important difference. A modal window describes parts of a UI that blocks user interaction. A dialog doesn't have to be blocking.
Usage
Non-modal
- Toast: informative but non-interruptive
Modal
No JavaScript required
In browsers that support Invoker Commands you can toggle a <dialog> without JavaScript by using
the commandfor and command attributes.
---import { Dialog } from "@opui/astro"import { Button } from "@opui/astro"---
<Button commandfor="example-dialog" command="show-modal" variant="outlined"> Open dialog</Button>
<Dialog id="example-dialog" role="alertdialog" aria-labelledby="dialog-heading" aria-modal="true"> <h2 id="dialog-heading" class="h4" slot="header">Are you sure?</h2> <p slot="content"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sodales, nulla sit amet porttitor rhoncus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sodales, nulla sit amet porttitor rhoncus. </p> <Fragment slot="actions"> <Button commandfor="example-dialog" command="close" type="button"> Cancel </Button> <Button commandfor="example-dialog" command="close" type="button" variant="filled" > Save </Button> </Fragment></Dialog><button commandfor="example-dialog" command="show-modal" class="button outlined"> Open dialog</button><dialog class="dialog card elevated" id="example-dialog" role="alertdialog" aria-labelledby="dialog-heading" aria-modal="true"> <hgroup><h2 id="dialog-heading" class="h4">Are you sure?</h2></hgroup> <div class="content"> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sodales, nulla sit amet porttitor rhoncus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus sodales, nulla sit amet porttitor rhoncus. </p> </div> <div class="actions"> <button commandfor="example-dialog" command="close" type="button" class="button" > Cancel</button ><button commandfor="example-dialog" command="close" type="button" class="button filled" > Save </button> </div></dialog>How to close a dialog
Use closedby prop to control the closing behavior.
| Prop | Description |
|---|---|
closedby="any" | Click anywhere outside of the dialog to close it. |
closedby="closerequest" | Device-specific way to close, ex: Esc on desktop, back button on mobile, and whatever dismiss action assistive tools use. |
closedby="none" | You have to handroll a closing solution yourself. |
---import { Dialog } from "@opui/astro"import { Radio } from "@opui/astro"import { Button } from "@opui/astro"import { FieldSet as Fieldset } from "@opui/astro"import { FieldGroup } from "@opui/astro"import { FieldLegend } from "@opui/astro"---
<Button commandfor="closing-behaviors-dialog" command="show-modal" variant="outlined"> Open dialog</Button>
<Dialog id="closing-behaviors-dialog" closedby="any"> <h2 class="h4" slot="header">How to close</h2> <div slot="content"> <Fieldset> <FieldLegend>Choose a closing behavior:</FieldLegend> <FieldGroup name="closedby-demo"> <Radio value="any" checked>any</Radio> <Radio value="closerequest">closerequest</Radio> <Radio value="none">none</Radio> </FieldGroup> </Fieldset> </div> <Fragment slot="actions"> <Button commandfor="closing-behaviors-dialog" command="close"> Close manually </Button> </Fragment></Dialog>
<script> const dialog = document.getElementById( "closing-behaviors-dialog", ) as HTMLDialogElement const radios = document.querySelectorAll('input[name="closedby-demo"]')
radios.forEach((radio) => { radio.addEventListener("change", (e) => { const target = e.target as HTMLInputElement if (dialog) { dialog.setAttribute("closedby", target.value) } }) })</script><script type="module"> const e = document.getElementById("closing-behaviors-dialog"), a = document.querySelectorAll('input[name="closedby-demo"]'); a.forEach((t) => { t.addEventListener("change", (o) => { const n = o.target; e && e.setAttribute("closedby", n.value); }); });</script><button commandfor="closing-behaviors-dialog" command="show-modal" class="button outlined"> Open dialog</button><dialog class="dialog card elevated" closedby="any" id="closing-behaviors-dialog"> <hgroup><h2 class="h4">How to close</h2></hgroup> <div class="content"> <div> <fieldset class="fieldset"> <legend>Choose a closing behavior:</legend> <div class="field-group" role="group"> <label class="radio"> <input type="radio" checked name="closedby-demo" value="any" /><span class="label" >any</span > </label> <label class="radio"> <input type="radio" name="closedby-demo" value="closerequest" /><span class="label">closerequest</span> </label> <label class="radio"> <input type="radio" name="closedby-demo" value="none" /><span class="label" >none</span > </label> </div> </fieldset> </div> </div> <div class="actions"> <button commandfor="closing-behaviors-dialog" command="close" class="button" > Close manually </button> </div></dialog>Accessibility
-
The
tabindexattribute must not be used on the<dialog>element.
Role & attributes
| Role/attribute | Usage |
|---|---|
role="dialog" | Identifies the element that serves as the dialog container. |
role="alertdialog" | If the dialog is a confirmation window communicating an important message that requires a confirmation or other user response. |
aria-labelledby="IDREF" | Gives the dialog an accessible name by referring to the element that provides the dialog title. |
aria-describedby="IDREF" | Gives the dialog an accessible description by referring to the dialog content that describes the primary message or purpose of the dialog. |
aria-modal="true" | Tells assistive technologies that the windows underneath the current dialog are not available for interaction (inert). |
Keyboard support
| Key | Function |
|---|---|
| Tab |
|
| Shift + Tab |
|
| Esc | Closes the dialog. |
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
closedby | "any" | "closerequest" | "none" | - | Specifies how the dialog can be closed (e.g., clicking outside). |
actions.align | "end" | - | Alignment of the actions slot. |
Slots
| Slot | Description |
|---|---|
header | The header content, wrapped in an hgroup. |
content | The main content, wrapped in a div with a content class. |
actions | The footer actions, wrapped in a div with an actions class. |
default | Any content not assigned to a named slot. |
Browser support
Full support Supported since v135. Partial support
Missing:
container-style-queries, overlay.
Partial support
Missing: dialog-closedby, overlay.
See also the full browser support guide.
Installation
Dependencies
@layer components.extended { :where(.dialog) { inline-size: 100%; inset: 0; margin: auto; margin-block-start: 15%; max-inline-size: calc(100% - var(--size-4)); padding-block: 0; pointer-events: none; position: fixed;
@media (width > 600px) { max-inline-size: 60ch; }
/* Animation */ /* There's no close animation, intentionally */ opacity: 0;
&::backdrop { backdrop-filter: blur(1px); background-color: rgba(0, 0, 0, 0.5);
@media (prefers-reduced-motion: reduce) { backdrop-filter: none; } }
&:not([open]) { display: none; }
&[open] { pointer-events: all; }
.actions { justify-content: end; padding-inline: var(--size-3) var(--size-1); }
&[open] { opacity: 1; transition: display 0.2s allow-discrete, overlay 0.2s allow-discrete, opacity 0.2s var(--ease-out-1);
@starting-style { opacity: 0; } }
@media (prefers-reduced-motion: no-preference) { &[open] { margin-block-start: 15%;
@starting-style { opacity: 0; } } } }
:where(html:has(.dialog[open])) { block-size: 100%; overflow: hidden; }}@layer components.root { :where(.card) { --_bg-tonal: var(--surface-tonal); --_bg-elevated: var(--surface-elevated); --_bg-surface: var(--surface-default); --_border-color: var(--border-color); --_card-bg-color: var(--_bg-surface); --_card-border-color: transparent; --_card-border-width: 0;
--_card-shadow: none; --_shadow-light: var(--shadow-3); --_shadow-dark: var(--shadow-4); --_shadow-elevated: var(--_shadow-light);
@container style(--color-scheme: dark) { --_shadow-elevated: var(--_shadow-dark); }
background-color: var(--_card-bg-color); border-color: var(--_card-border-color); border-radius: var(--border-radius); border-style: solid; border-width: var(--_card-border-width); box-shadow: var(--_card-shadow); display: flex; flex-direction: column; gap: var(--size-3); overflow: hidden; padding-inline: 0; position: relative;
/* Variants */ &.text { --_card-bg-color: transparent; --_card-border-color: transparent; --_card-border-width: 0; --_card-shadow: none; }
&.tonal { --_card-bg-color: var(--_bg-tonal); --_card-border-width: 1px; }
&.elevated { --_card-bg-color: var(--_bg-elevated); --_card-shadow: var(--_shadow-elevated); }
&.outlined { --_card-bg-color: var(--_bg-surface); --_card-border-color: var(--_border-color); --_card-border-width: 1px; }
&> :where(hgroup, .content) { padding-inline: var(--size-3);
&:last-child { padding-block-end: var(--size-3); } }
/* Header */ &>hgroup { padding-block: var(--size-3) 0;
/* Top paragraph */ &>p:first-of-type:first-child { line-height: 1.3; }
:where(h1, h2, h3, h4, h5, h6):last-child { margin-block-end: 0; }
/* Bottom paragraph */ &>p:last-of-type:last-child:not(:first-child) { font-size: var(--font-size-1); } }
/* Content */ &>.content:where(:only-child, :first-child) { padding-block: var(--size-3) var(--size-4); }
/* Actions */ &>.actions { display: flex; gap: var(--size-2); margin-block: var(--size-2) 0; padding-block-end: var(--size-2); padding-inline: var(--size-3);
&:has(.button:first-child[class="button"]) { padding-inline: var(--size-1) var(--size-3); }
&:has(.button:not([class="button"])) { padding-block-end: var(--size-2); }
/* Alignment */ &.align-end { justify-content: end;
&:has(.button:first-child[class="button"]) { padding-inline: var(--size-3) var(--size-1); } } }
}}