Base components
Dialog
BetaSince 0.2.0
A modal window that blocks the rest of the page until it is closed.
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
DialogClose,
} from "@anantya/uix";Overview
Use a dialog for a short task or a decision the reader must finish before going on, such as editing a profile or confirming a delete. It opens over the page and blocks the rest of it until it is closed.
For information that shouldn't stop the reader, show it on the page instead. For a long form or a task of several steps, use a page of its own.
Playground
Note
The playground forDialog is coming soon.Examples
A task with Cancel and a main action in DialogFooter, in the default size, md.
Code
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
} from "@anantya/uix";
export default function EditProfile() {
return (
<Dialog>
<DialogTrigger>Edit profile</DialogTrigger>
<DialogContent>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Changes show on your public page.</DialogDescription>
<DialogFooter cancel="Cancel" action="Save changes" />
</DialogContent>
</Dialog>
);
}A confirmation in size sm: DialogHeader puts a warning icon before the title, and a
danger action confirms it.
Code
import { AlertTriangle } from "@anantya/icons";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@anantya/uix";
export default function DeleteInvoice() {
return (
<Dialog>
<DialogTrigger variant="danger">Delete invoice</DialogTrigger>
<DialogContent size="sm">
<DialogHeader start={<AlertTriangle className="text-danger" />}>
<DialogTitle>Delete this invoice?</DialogTitle>
<DialogDescription>
Mehta Textiles can no longer pay INV-2026-0142 from their link.
</DialogDescription>
</DialogHeader>
<DialogFooter cancel="Keep invoice" action={{ children: "Delete", variant: "danger" }} />
</DialogContent>
</Dialog>
);
}When action's onClick returns a promise, the button shows it is loading and the dialog
closes once the promise resolves.
Code
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
} from "@anantya/uix";
const sendReminder = () => new Promise((resolve) => setTimeout(resolve, 1500));
export default function SendReminder() {
return (
<Dialog>
<DialogTrigger variant="secondary">Send reminder</DialogTrigger>
<DialogContent size="sm">
<DialogTitle>Send a reminder?</DialogTitle>
<DialogDescription>
Mehta Textiles gets an email about INV-2026-0142, due 17 Nov 2026.
</DialogDescription>
<DialogFooter
cancel="Cancel"
action={{ children: "Send reminder", loadingText: "Sending…", onClick: sendReminder }}
/>
</DialogContent>
</Dialog>
);
}In size lg, long content in DialogBody scrolls while the title and actions stay in view.
closeOnBackdropClick={false} keeps a stray click from closing it.
Code
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
} from "@anantya/uix";
export default function TermsOfService() {
return (
<Dialog>
<DialogTrigger variant="secondary">Read the terms</DialogTrigger>
<DialogContent size="lg" closeOnBackdropClick={false}>
<DialogTitle>Terms of service</DialogTitle>
<DialogDescription>Read to the end to accept them.</DialogDescription>
<DialogBody>
{Array.from({ length: 30 }, (_, index) => index + 1).map((section) => (
<p key={section}>
Section {section}. You keep the rights to everything you upload, and give us the
rights we need to store it, show it to the people you share it with, and back it up.
</p>
))}
</DialogBody>
<DialogFooter cancel="Decline" action="Accept" />
</DialogContent>
</Dialog>
);
}Size full fills the screen, such as for a preview, with its actions at the bottom.
Code
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
} from "@anantya/uix";
export default function InvoicePreview() {
return (
<Dialog>
<DialogTrigger variant="secondary">Preview invoice</DialogTrigger>
<DialogContent size="full">
<DialogTitle>Invoice INV-2026-0142</DialogTitle>
<DialogDescription>Mehta Textiles, ₹12,500, due 17 Nov 2026.</DialogDescription>
<DialogBody>
<p>Cotton fabric, 50 metres at ₹250 a metre.</p>
</DialogBody>
<DialogFooter>
<DialogClose>Done</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}A dialog opened from another one shows on top of it, and Esc closes the top one first.
Code
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
DialogTrigger,
} from "@anantya/uix";
export default function NestedDialogs() {
return (
<Dialog>
<DialogTrigger variant="secondary">Project settings</DialogTrigger>
<DialogContent>
<DialogTitle>Project settings</DialogTitle>
<DialogDescription>Settings for everyone in the project.</DialogDescription>
<DialogFooter>
<Dialog>
<DialogTrigger variant="danger">Delete project</DialogTrigger>
<DialogContent size="sm">
<DialogTitle>Delete project?</DialogTitle>
<DialogDescription>Its files are deleted for everyone.</DialogDescription>
<DialogFooter>
<DialogClose>Keep project</DialogClose>
<DialogClose variant="danger">Delete</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<DialogClose>Done</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}Props
Note
The props table forDialogContent is coming soon.Accessibility
It follows the WAI-ARIA dialog (modal) pattern, on a <dialog> opened with showModal(), so the rest of the page is inert while it is open.
| Key | Action |
|---|---|
| Enter or Space | On the trigger, opens the dialog and moves focus into it. |
| Tab | Moves focus to the next element in the dialog, from the last back to the first. |
| Shift + Tab | Moves focus to the previous element in the dialog, from the first back to the last. |
| Escape | Closes the dialog opened last and returns focus to its trigger. While its action runs, it stays open. |
Focus
- Opening it focuses the first focusable element, or the one
initialFocusnames. When long content would push that element out of view, it focuses the top of the dialog instead, so the reader starts at the title - Closing it returns focus to its trigger
- A scrolling DialogBody is a Tab stop, so the keyboard can scroll it too
Screen readers
- A screen reader announces it as a dialog named by its DialogTitle and described by its DialogDescription, such as "Edit profile, dialog, Changes show on your public page."
- The rest of the page is hidden from screen readers while it is open
- Its close button in the corner is named Close; change it with
closeLabel, such as for another language - While an action that returns a promise runs, its button is marked busy and Cancel is disabled
Contrast
Its buttons have the contrast of their Button variants. The text and the close button on the dialog's surface:
| Part | Light | Dark |
|---|---|---|
| DialogTitle | 17.7:1 | 16.9:1 |
| DialogDescription | 7.7:1 | 6.7:1 |
| Close button, and on hover | 16.1:1 | 14.2:1 |
| Focus outline | 5.2:1 | 6.7:1 |
The backdrop is decorative, so it has no contrast minimum.
WCAG 2.2
- 1.3.1 Info and Relationships: DialogTitle names it and DialogDescription describes it
- 1.4.3 Contrast (Minimum) and 1.4.11 Non-text Contrast, in light and dark
- 1.4.10 Reflow: on a phone it sits 16px from the screen's sides, with its actions stacked
- 2.1.1 Keyboard and 2.1.2 No Keyboard Trap: Tab stays inside while it is open, and Esc closes it
- 2.4.3 Focus Order: focus moves into it on open and back to the trigger on close
- 2.4.7 Focus Visible: a focus outline shows on keyboard focus
- 4.1.2 Name, Role, Value: a modal dialog with its name and description
axe checks every example on this page open, in CI, in light and dark, in Chromium, Firefox and WebKit.
What your app still has to do
- Give every dialog a DialogTitle that names its task. It is required: in development, DialogContent throws without one
- Name its actions after what they do, such as Delete invoice, not OK
- Tell the reader what happened after it closes, such as with
announce("Reminder sent") - Show an error inside the dialog when its action fails, so the reader can try again without losing what they entered
Do / Don't
Name the dialog after its task in DialogTitle, such as Delete invoice.
Don't use a vague title such as Are you sure? A screen reader user hears it with nothing to go on.
Name the confirm button after what it does, such as Delete invoice.
Don't use OK and Cancel for a destructive action. The reader can't tell what OK does.
Keep it to one short task, and put a long form or several steps on a page.
Don't open a dialog without the reader asking for it, such as on page load. It interrupts what they came to do.
Changelog
Every change to Dialog since 0.2.0 is in the release notes.