Components
Dialog
Full support Supported since v135. Partial support
Missing:
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.
<script setup lang="ts">import { Button, Dialog } from "opui-css/vue"</script>
<template> <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" > <template #header ><h2 id="dialog-heading" class="ui-h4">Are you sure?</h2></template > <template #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></template > <template #actions> <Button commandfor="example-dialog" command="close" type="button"> Cancel </Button> <Button commandfor="example-dialog" command="close" type="button" variant="filled" > Save </Button> </template> </Dialog></template><!--[--><button class="ui-button ui-outlined" commandfor="example-dialog" command="show-modal"> <!--[--> Open dialog <!--]--></button><dialog class="ui-dialog ui-card ui-elevated" id="example-dialog" role="alertdialog" aria-labelledby="dialog-heading" aria-modal="true"> <hgroup> <!--[--> <h2 id="dialog-heading" class="ui-h4">Are you sure?</h2> <!--]--> </hgroup> <div class="ui-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="ui-actions"> <!--[--><button class="ui-button" commandfor="example-dialog" command="close" type="button" > <!--[--> Cancel <!--]--></button ><button class="ui-button ui-filled" commandfor="example-dialog" command="close" type="button" > <!--[--> 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. |
<script setup lang="ts">import { Button, Dialog, FieldGroup, FieldLegend, FieldSet, Radio,} from "opui-css/vue"import { onMounted } from "vue"
onMounted(() => { 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 if (dialog) { dialog.setAttribute("closedby", target.value) } }) })})</script>
<template> <Button commandfor="closing-behaviors-dialog" command="show-modal" variant="outlined" > Open dialog </Button>
<Dialog id="closing-behaviors-dialog" closedby="any"> <template #header><h2 class="ui-h4">How to close</h2></template> <template #content ><div> <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></template > <template #actions> <Button commandfor="closing-behaviors-dialog" command="close"> Close manually </Button> </template> </Dialog></template><!--[--><button class="ui-button ui-outlined" commandfor="closing-behaviors-dialog" command="show-modal"> <!--[--> Open dialog <!--]--></button><dialog class="ui-dialog ui-card ui-elevated" id="closing-behaviors-dialog" closedby="any"> <hgroup> <!--[--> <h2 class="ui-h4">How to close</h2> <!--]--> </hgroup> <div class="ui-content"> <!--[--> <div> <fieldset class="ui-fieldset"> <!--[--> <legend class=""><!--[-->Choose a closing behavior:<!--]--></legend> <div class="ui-field-group" role="group"> <!--[--><label class="ui-radio" ><input type="radio" value="any" aria-describedby="s1-0" /><span class="ui-label" ><!--[-->any<!--]--></span ><!----></label ><label class="ui-radio" ><input type="radio" value="closerequest" aria-describedby="s1-1" /><span class="ui-label"><!--[-->closerequest<!--]--></span ><!----></label ><label class="ui-radio" ><input type="radio" value="none" aria-describedby="s1-2" /><span class="ui-label" ><!--[-->none<!--]--></span ><!----></label ><!--]--> </div> <!--]--> </fieldset> </div> <!--]--> </div> <!--[--><!--]--> <div class="ui-actions"> <!--[--><button class="ui-button" commandfor="closing-behaviors-dialog" command="close" > <!--[--> 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 |
|---|---|---|---|
actionsAlign | "start", "end" | - | Alignment of the actions slot. |
closedby | "any" | "closerequest" | "none" | - | Specifies how the dialog can be closed (e.g., clicking outside). |
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:
overlay.
Partial support
Missing: dialog-closedby, overlay.
See also the full browser support guide.
Installation
Dependencies
@layer components.extended { :where(.ui-dialog) { --_motion: var(--motion, 1); 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); }
&:not([open]) { display: none; }
&[open] { pointer-events: all; }
.ui-actions { justify-content: end; padding-inline: var(--size-3) var(--size-1); }
&[open] { opacity: 1; transition: display calc(0.2s * var(--_motion)) allow-discrete, overlay calc(0.2s * var(--_motion)) allow-discrete, opacity calc(0.2s * var(--_motion)) var(--ease-out-1);
@starting-style { opacity: 0; } }
&[open] { margin-block-start: 15%;
@starting-style { opacity: 0; } } }
:where(html:has(.ui-dialog[open])) { block-size: 100%; overflow: hidden; }}@layer components.root { :where(.ui-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 */ &.ui-text { --_card-bg-color: transparent; --_card-border-color: transparent; --_card-border-width: 0; --_card-shadow: none; }
&.ui-tonal { --_card-bg-color: var(--_bg-tonal); --_card-border-width: 1px; }
&.ui-elevated { --_card-bg-color: var(--_bg-elevated); --_card-shadow: var(--_shadow-elevated); }
&.ui-outlined { --_card-bg-color: var(--_bg-surface); --_card-border-color: var(--_border-color); --_card-border-width: 1px; }
& > :where(hgroup, .ui-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 */ & > .ui-content:where(:only-child, :first-child) { padding-block: var(--size-3) var(--size-4); }
/* Actions */ & > .ui-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(.ui-button:first-child[class="ui-button"]) { padding-inline: var(--size-1) var(--size-3); }
&:has(.ui-button:not([class="ui-button"])) { padding-block-end: var(--size-2); }
/* Alignment */ &.ui-align-end { justify-content: end;
&:has(.ui-button:first-child[class="ui-button"]) { padding-inline: var(--size-3) var(--size-1); } } } }}