Skip to main content

Theme config

Theme mode

Color palette

Grays

Border radii/radiuses/radiopedes/you know
Border radius
Field border radius
Button border radius
All
Components
Guides
API
Recent

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.

Usage

Non-modal

  • Toast: informative but non-interruptive

No JavaScript required

In browsers that support Invoker Commands you can toggle a <dialog> without JavaScript by using the commandfor and command attributes.

Are you sure?

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.

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

How to close

Choose a closing behavior:
---
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 tabindex attribute 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
  • Moves focus to next focusable element inside the dialog.
  • When focus is on the last focusable element in the dialog, moves focus to the first focusable element in the dialog.
Shift + Tab
  • Moves focus to previous focusable element inside the dialog.
  • When focus is on the first focusable element in the dialog, moves focus to the last focusable element in the dialog.
Esc Closes the dialog.

Source: w3.org, MDN

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);
}
}
}
}
}