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

    Full supportSupported since v135.Partial supportMissing: overlay.Partial supportMissing: 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="ui-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="ui-button ui-outlined"
    >
    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
    commandfor="example-dialog"
    command="close"
    type="button"
    class="ui-button"
    >
    Cancel</button
    ><button
    commandfor="example-dialog"
    command="close"
    type="button"
    class="ui-button ui-filled"
    >
    Save
    </button>
    </div>
    </dialog>

    How to close a dialog

    Use closedby prop to control the closing behavior.

    PropDescription
    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="ui-h4" slot="header">How to close</h2>
    <Fieldset slot="content">
    <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>
    <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>
    <button
    commandfor="closing-behaviors-dialog"
    command="show-modal"
    class="ui-button ui-outlined"
    >
    Open dialog
    </button>
    <dialog
    class="ui-dialog ui-card ui-elevated"
    closedby="any"
    id="closing-behaviors-dialog"
    >
    <hgroup><h2 class="ui-h4">How to close</h2></hgroup>
    <div class="ui-content">
    <fieldset slot="content" class="ui-fieldset">
    <legend>Choose a closing behavior:</legend>
    <div class="ui-field-group" role="group">
    <label class="ui-radio"
    ><input name="closedby-demo" type="radio" value="any" checked /><span
    class="ui-label"
    >any</span
    ></label
    ><label class="ui-radio"
    ><input name="closedby-demo" type="radio" value="closerequest" /><span
    class="ui-label"
    >closerequest</span
    ></label
    ><label class="ui-radio"
    ><input name="closedby-demo" type="radio" value="none" /><span
    class="ui-label"
    >none</span
    ></label
    >
    </div>
    </fieldset>
    </div>
    <div class="ui-actions">
    <button
    commandfor="closing-behaviors-dialog"
    command="close"
    class="ui-button"
    >
    Close manually
    </button>
    </div>
    </dialog>
    <script type="module">
    var e = document.getElementById(`closing-behaviors-dialog`);
    document.querySelectorAll(`input[name="closedby-demo"]`).forEach((t) => {
    t.addEventListener(`change`, (t) => {
    let n = t.target;
    e && e.setAttribute(`closedby`, n.value);
    });
    });
    </script>

    Accessibility

    • The tabindex attribute must not be used on the<dialog> element.

    Role & attributes

    Role/attributeUsage
    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

    KeyFunction
    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.
    EscCloses the dialog.

    Source: w3.org, MDN

    API

    Dialog API

    PropTypeDefaultDescription
    actionsAlign"start", "end"-Alignment for the actions.
    closedby"any", "closerequest", "none"-How the dialog can be closed. "any" also closes it on a click outside.

    Slots

    SlotDescription
    actionsA group of actions, such as buttons.
    contentThe dialog content.
    defaultRaw content placed directly in the dialog.
    headerThe dialog header.

    Browser support

    Full supportSupported since v135.Partial supportMissing: overlay.Partial supportMissing: 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 {
    font-size: var(--font-size-0);
    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);
    }
    }
    }
    }
    }