Skip to content

Dialogs

A thin wrapper around <dialog> + showModal(). The browser handles the focus trap, scroll lock, Esc dismissal, and ::backdrop — the wrapper just bridges React’s open prop to the imperative API and exposes a Card-style shorthand surface.

The trigger pattern uses the Invoker Commands API (commandfor + command="show-modal"): a <button> paired with a dialog id. No JavaScript glue, no React state. The wrapper still accepts a controlled open / onOpenChange pair for state-driven flows — see Controlled state.

Open with a button, close via Esc, backdrop click, or the X. closedby="any" is the default.

Invite teammate

They'll receive an email with a sign-up link.

Pick a role after they accept.

<button
type="button"
class="btn btn-primary"
commandfor="dialog-basic"
command="show-modal"
>
Open dialog
</button>
<dialog id="dialog-basic" class="dialog" closedby="any">
<div class="dialog-header">
<h2 class="dialog-title">Invite teammate</h2>
<button
type="button"
class="dialog-close"
commandfor="dialog-basic"
command="close"
aria-label="Close"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<p class="dialog-description">
They'll receive an email with a sign-up link.
</p>
<div class="dialog-body">
<p>Pick a role after they accept.</p>
</div>
<div class="dialog-footer">
<button
type="button"
class="btn btn-ghost"
commandfor="dialog-basic"
command="close"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
commandfor="dialog-basic"
command="close"
>
Send invite
</button>
</div>
</dialog>

size="sm" for confirms, "md" (default) for short forms, "lg" for longer ones.

Small

Up to 24rem wide.

Medium

Default — up to 32rem.

Large

Up to 48rem — room for two-column forms.
<button type="button" class="btn" commandfor="dialog-sm" command="show-modal">
Small
</button>
<button type="button" class="btn" commandfor="dialog-md" command="show-modal">
Medium
</button>
<button type="button" class="btn" commandfor="dialog-lg" command="show-modal">
Large
</button>
<dialog id="dialog-sm" class="dialog dialog-sm">
<div class="dialog-header">
<h2 class="dialog-title">Small</h2>
</div>
<div class="dialog-body">Up to 24rem wide.</div>
<div class="dialog-footer">
<button type="button" class="btn" commandfor="dialog-sm" command="close">
Close
</button>
</div>
</dialog>
<dialog id="dialog-md" class="dialog">
<div class="dialog-header">
<h2 class="dialog-title">Medium</h2>
</div>
<div class="dialog-body">Default — up to 32rem.</div>
<div class="dialog-footer">
<button type="button" class="btn" commandfor="dialog-md" command="close">
Close
</button>
</div>
</dialog>
<dialog id="dialog-lg" class="dialog dialog-lg">
<div class="dialog-header">
<h2 class="dialog-title">Large</h2>
</div>
<div class="dialog-body">Up to 48rem — room for two-column forms.</div>
<div class="dialog-footer">
<button type="button" class="btn" commandfor="dialog-lg" command="close">
Close
</button>
</div>
</dialog>

Wrap the body in <form method="dialog"> — submitting any button inside closes the dialog, and the submitter’s value lands on <dialog>.returnValue.

Rename project

<button
type="button"
class="btn btn-primary"
commandfor="dialog-form"
command="show-modal"
>
Rename project
</button>
<dialog id="dialog-form" class="dialog">
<form method="dialog">
<div class="dialog-header">
<h2 class="dialog-title">Rename project</h2>
<button
type="button"
class="dialog-close"
commandfor="dialog-form"
command="close"
aria-label="Close"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div class="dialog-body">
<div class="field">
<label class="field-label" for="dialog-form-name">Name</label>
<input
id="dialog-form-name"
class="input input-bordered"
name="name"
required
/>
</div>
</div>
<div class="dialog-footer">
<button
type="submit"
class="btn btn-ghost"
value="cancel"
formnovalidate
>
Cancel
</button>
<button type="submit" class="btn btn-primary" value="save">Save</button>
</div>
</form>
</dialog>

For irreversible actions, set closedby="closerequest". Backdrop clicks no longer dismiss — the user has to make an explicit choice.

Delete project?

This permanently removes all data. This cannot be undone.

<button
type="button"
class="btn btn-danger"
commandfor="dialog-destroy"
command="show-modal"
>
Delete project
</button>
<dialog id="dialog-destroy" class="dialog dialog-sm" closedby="closerequest">
<div class="dialog-header">
<h2 class="dialog-title">Delete project?</h2>
</div>
<p class="dialog-description">
This permanently removes all data. This cannot be undone.
</p>
<div class="dialog-footer">
<button
type="button"
class="btn btn-ghost"
commandfor="dialog-destroy"
command="close"
>
Cancel
</button>
<button
type="button"
class="btn btn-danger"
commandfor="dialog-destroy"
command="close"
>
Delete
</button>
</div>
</dialog>

<Dialog> is opinionated — single header, single body, single footer. For layouts that don’t fit (media block, multiple bodies, segmented content), drop down to <Dialog.Container>: it renders the bare <dialog> element with size + closedby props, and lets you compose subparts yourself.

Workspace settings

<Button commandfor="dialog-custom-r" command="show-modal">
Settings
</Button>
<Dialog.Container id="dialog-custom-r" size="lg">
<Dialog.Header>
<Dialog.Title>Workspace settings</Dialog.Title>
<Dialog.CloseButton />
</Dialog.Header>
<Dialog.Body>
<Field>
<Field.Label>Name</Field.Label>
<Input defaultValue="Acme Inc." />
</Field>
<Field>
<Field.Label>Billing email</Field.Label>
<Input type="email" defaultValue="ops@acme.test" />
</Field>
</Dialog.Body>
<Dialog.Footer>
<Button variant="ghost" commandfor="dialog-custom-r" command="close">
Cancel
</Button>
<Button commandfor="dialog-custom-r" command="close">
Save changes
</Button>
</Dialog.Footer>
</Dialog.Container>

<Dialog.Title> accepts the icon prop in the advanced path.

When the open state lives in React (multi-step flow, async submit, deep-linked routes), pair open with onOpenChange. The wrapper bridges to showModal() / close() and forwards the native close event back through onOpenChange(false).

import { useState } from "react";
import { Button, Dialog } from "@aortl/admin-react";
function ConfirmDelete({ onConfirm }: { onConfirm: () => void }) {
const [open, setOpen] = useState(false);
return (
<>
<Button variant="danger" onClick={() => setOpen(true)}>
Delete
</Button>
<Dialog
open={open}
onOpenChange={setOpen}
size="sm"
closedby="closerequest"
title="Delete project?"
description="This cannot be undone."
actions={
<>
<Button variant="ghost" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="danger"
onClick={() => {
onConfirm();
setOpen(false);
}}
>
Delete
</Button>
</>
}
/>
</>
);
}