Skip to content
Components, switch product

Dialog

<gui-dialog> shows content the user must deal with before going back to the page, such as a short form or a confirmation. The dialog is your own <dialog> element, so the browser does the hard parts: it shows it above everything, makes the rest of the page inert, closes it on Escape and returns the focus to what opened it. The element opens it as a modal, places it in the middle of the viewport or along one edge, locks the page’s scroll while it is open, and fires an event when it opens and closes.

Rename board

The board is called Q3 roadmap.

Code of the example above
DialogBasic.tsx
import { useState, type FormEvent } from 'react';
import {
GuiButton,
GuiDialog,
GuiTextinput,
} from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function DialogBasic() {
const [name, setName] = useState('Q3 roadmap');
// The form closes the dialog. Save renames the board, Cancel leaves it.
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
const submitter = (event.nativeEvent as SubmitEvent).submitter;
if ((submitter as HTMLButtonElement | null)?.value !== 'save') return;
setName(String(new FormData(event.currentTarget).get('board-name')));
};
return (
<>
<GuiButton
label="Rename board"
variant="outlined"
commandFor="rename-board"
command="show-modal"
/>
<GuiDialog>
<dialog id="rename-board" aria-labelledby="rename-board-title">
<form method="dialog" className="gui-grid" onSubmit={onSubmit}>
<h2 id="rename-board-title">Rename board</h2>
<GuiTextinput label="Board name" name="board-name" value="Q3 roadmap" />
<div className="gui-grid gui-grid--row gui-grid--justify-start">
<div className="gui-grid__cell">
<GuiButton type="submit" label="Save" value="save" />
</div>
<div className="gui-grid__cell">
<GuiButton
type="submit"
label="Cancel"
value="cancel"
variant="outlined"
/>
</div>
</div>
</form>
</dialog>
</GuiDialog>
<p id="board-name-status" role="status">
The board is called {name}.
</p>
</>
);
}

The element’s entry point is @golemui/gui-components/dialog, which registers <gui-dialog> and nothing else. To install the package and load the stylesheet, see Installation.

The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.

DialogExample.tsx
import { GuiDialog } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function DialogExample() {
return (
<GuiDialog
>
<dialog id="rename-board" aria-labelledby="rename-board-title">…</dialog>
</GuiDialog>
);
}

The dialog is the element’s <dialog> child. Name it with aria-labelledby, pointing at its heading, or with aria-label: the element logs a warning in development when it opens a dialog with no name.

Open it in one of two ways:

  • From a button, with no script. A gui-button or a native <button> with commandfor, the dialog’s id, and command="show-modal" opens it, and one with command="close" closes it. In React, they are the commandFor and command props of GuiButton: React’s types don’t know them on a native <button> yet.
  • From your code. Set open to true or false, as in Confirming an action.

A <form method="dialog"> inside closes the dialog when the user submits it, after the form’s checks pass, and sets the dialog’s returnValue to the value of the button that submitted it. gui-button with type="submit" submits it like a native button.

The element adds nothing to your markup but the alertdialog role when you ask for it, so a server-rendered page shows the same markup before and after the element upgrades. A closed dialog stays hidden until then.

Properties

Property Type Default Description
open boolean – Whether the dialog is open. Without a value it takes the dialog's own state.
placement GuiDialogPlacement – center (the default), or a drawer along the start, end, top or bottom edge. It reflects, for the stylesheet: set from a framework as a property, it still places the dialog.
dismissible boolean – Whether a click on the backdrop closes the dialog. An alert dialog ignores it.
alert boolean – Whether the dialog is an alert dialog: it gets the alertdialog role.
dialog read-only, no attribute HTMLDialogElement | null – The app's dialog: the element's <dialog> child.

Events

Every event bubbles and is composed, so you can listen on any ancestor.

Event Description
gui-toggle The dialog opened or closed, from an invoker, Escape, a backdrop click or a button that closes it. detail.open is the new state. Setting open fires nothing.

placement puts the dialog along one edge of the viewport, as a drawer, as tall as the viewport (or as wide, at the top or the bottom), sliding in from that edge: start, end, top or bottom. Start and end follow the reading direction. The default, center, is a dialog in the middle of the viewport.

dismissible lets a click on the backdrop, outside the dialog, close it. It asks the dialog to close the way Escape does, so a cancel listener that calls preventDefault() can keep it open, for example while a form inside has unsaved changes.

Filters

Code of the example above
DialogDrawer.tsx
import {
GuiButton,
GuiCheckbox,
GuiDialog,
} from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function DialogDrawer() {
return (
<>
<GuiButton
label="Filters"
variant="outlined"
commandFor="board-filters"
command="show-modal"
/>
<GuiDialog placement="end" dismissible>
<dialog id="board-filters" aria-labelledby="board-filters-title">
<form method="dialog" className="gui-grid">
<h2 id="board-filters-title">Filters</h2>
<GuiCheckbox label="Assigned to me" name="mine" />
<GuiCheckbox label="Due this week" name="due" />
<GuiCheckbox label="Show completed" name="completed" />
<div className="gui-grid gui-grid--row gui-grid--justify-start">
<div className="gui-grid__cell">
<GuiButton type="submit" label="Apply filters" />
</div>
</div>
</form>
</dialog>
</GuiDialog>
</>
);
}

alert gives the dialog the alertdialog role, for a dialog that asks the user to confirm or acknowledge something urgent, such as a deletion. Screen readers announce it as an alert, and a click on the backdrop never closes it, even with dismissible: the user answers it.

This example opens the dialog from your code, setting open when the button is clicked, and reads the user’s answer from the button that submitted the form.

Delete this card?

Its comments and attachments go with it. You can't undo this.

Code of the example above
DialogConfirm.tsx
import { useState, type FormEvent } from 'react';
import { GuiButton, GuiDialog } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function DialogConfirm() {
const [open, setOpen] = useState(false);
const [deleted, setDeleted] = useState(false);
// The form closes the dialog. The button that submitted it says what the user chose.
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
const submitter = (event.nativeEvent as SubmitEvent).submitter;
if ((submitter as HTMLButtonElement | null)?.value === 'delete') setDeleted(true);
};
return (
<>
<GuiButton
id="delete-card"
label="Delete card"
variant="outlined"
onClick={() => setOpen(true)}
/>
<GuiDialog
id="delete-dialog"
alert
open={open}
onGuiToggle={(event) => setOpen(event.detail.open)}
>
<dialog aria-labelledby="delete-title" aria-describedby="delete-text">
<form method="dialog" className="gui-grid" onSubmit={onSubmit}>
<h2 id="delete-title">Delete this card?</h2>
<p id="delete-text">Its comments and attachments go with it. You can&apos;t undo this.</p>
<div className="gui-grid gui-grid--row gui-grid--justify-start">
<div className="gui-grid__cell">
<GuiButton type="submit" label="Cancel" value="cancel" variant="outlined" />
</div>
<div className="gui-grid__cell">
<GuiButton type="submit" label="Delete card" value="delete" />
</div>
</div>
</form>
</dialog>
</GuiDialog>
<p id="delete-status" role="status">
{deleted ? 'The card was deleted.' : ''}
</p>
</>
);
}

When the dialog opens or closes, from an invoker button, Escape, a backdrop click or a form inside, the element fires gui-toggle with the new state in event.detail.open, and open follows. Setting open yourself fires no gui-toggle. In a framework that holds open in its state, update that state from gui-toggle, as the examples do: otherwise, after the user closes the dialog, setting it to true again changes nothing.

While a dialog is open, the rest of the page is inert, and so would be the toasts the page shows. While a gui-dialog is open, the toaster shows its toasts inside it, so the user can still read them and use their buttons. A <dialog> you open with showModal() yourself, outside a gui-dialog, hides them.

<gui-dialog> has no box of its own (display: contents). The stylesheet draws your <dialog> as a card over a dimmed backdrop, and while it is open the element has the open attribute:

gui-dialog
<gui-dialog placement="end" open>
<dialog id="board-filters" aria-labelledby="board-filters-title" open>
<h2 id="board-filters-title">Filters</h2>
<!-- Your content. -->
</dialog>
</gui-dialog>

placement reflects as an attribute, so you can select a drawer as gui-dialog[placement='end'] > dialog. A centered dialog is --gui-dialog-size wide, and a drawer --gui-drawer-size, never more than the viewport. They have a set width rather than the width of their content, so a form inside has room. Content taller than the viewport scrolls inside the dialog.

styles.css
/* A wider dialog for the board settings, and a lighter backdrop everywhere. */
gui-dialog.board-settings > dialog {
--gui-dialog-size: 44rem;
}
:root {
--gui-dialog-backdrop: rgb(0 0 0 / 0.25);
}

While a dialog is open, the page under it doesn’t scroll: the stylesheet sets overflow: hidden on html.

The dialog reads these design tokens. Set them on :root to change every dialog, or on any element to change the dialogs inside it:

TokenWhat it styles
--gui-dialog-sizeThe width of a centered dialog, 32rem by default.
--gui-drawer-sizeThe width of a start or end drawer, the height of the others, 24rem by default.
--gui-dialog-backdropThe backdrop, rgb(0 0 0 / 0.45) by default.
--gui-bg-defaultThe background.
--gui-border-defaultThe border.
--gui-radius-lgThe corner radius of a centered dialog. A drawer has square corners.
--gui-shadow-lgThe shadow.
--gui-text-defaultThe text.
--gui-space-5The padding.
--gui-space-4The space a centered dialog keeps from the edges of the viewport.
--gui-overlay-durationHow long the dialog takes to fade and scale in, or a drawer to slide in. Reduced motion skips it.

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

  • Modal. The dialog is a native modal <dialog>: the browser makes the rest of the page inert, so the focus and screen readers stay in the dialog until it closes.
  • Focus. Opening the dialog moves the focus into it, to its first focusable element or the one with autofocus. Closing it returns the focus to the element that had it before, such as the button that opened it.
  • Name. Name the dialog with aria-labelledby or aria-label. For an alert dialog, point aria-describedby at its message too, so screen readers read it when the dialog opens.
  • Alert dialogs. alert gives the dialog the alertdialog role, unless you set a role yourself. Put the focus on the least destructive answer first, as the example does with Cancel.
KeyAction
TabMoves between the dialog’s controls. The page behind is inert.
EscapeCloses the dialog, unless a cancel listener prevents it.

What every element shares is in Accessibility.