Dialog
Modal dialog built on the native dialog element.
import { Dialog, DialogTrigger, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, DialogContent } from '@elirobinson/react/components/organisms/Dialog';Styles: @elirobinson/react/styles/organisms/Dialog.css — already included when you import @elirobinson/react/styles.css.
Show code
import { Button } from '@elirobinson/react/components/atoms/Button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@elirobinson/react/components/organisms/Dialog';
export default function Basic() {
return (
<Dialog>
<DialogTrigger className="ds-button ds-button--primary">Get in touch</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Contact Eli</DialogTitle>
<DialogDescription>
Practical AI consulting and tech support. No contracts required.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose />
<Button variant="accent">Send message</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}When to use it
Use Dialog when something needs to fully interrupt the page — confirming a destructive
action, collecting the minimum info needed to continue, a message that has to be dismissed
before anything else happens. It's built on the real <dialog> element, so the modality (focus
trap, Escape-to-close, blocking the rest of the page) comes from the browser, not from custom
JavaScript.
For content that's useful but skippable — a hint, a filter panel, extra detail on click — use
Popover instead. Nothing about it blocks the page.
Controlled
Open a dialog from outside without rendering a DialogTrigger at all — pass open and
onOpenChange and drive it from wherever the action originates, such as a row action or a
destructive-confirmation flow.
Show code
import { useState } from 'react';
import { Button } from '@elirobinson/react/components/atoms/Button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@elirobinson/react/components/organisms/Dialog';
export default function Controlled() {
const [open, setOpen] = useState(false);
return (
<>
<Button variant="secondary" onClick={() => setOpen(true)}>
Delete guide
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete "U8 Soccer Season Plan"?</DialogTitle>
<DialogDescription>
This removes the guide from your library. Anyone who already downloaded it keeps their
copy.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<Button variant="primary" onClick={() => setOpen(false)}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | false | — |
onOpenChange | ((open: boolean) => void) | — | — |
open | boolean | — | — |
DialogTrigger
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLButtonElement> props.
DialogHeader
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLDivElement> props.
DialogTitle
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLHeadingElement> props.
DialogDescription
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLParagraphElement> props.
DialogFooter
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLDivElement> props.
DialogClose
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLButtonElement> props.
DialogContent
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLDialogElement> props.
Accessibility
- Built on the native
<dialog>element viashowModal()/close()— not a custom-built modal. The browser supplies the modal semantics, the focus trap, and the top-layer stacking. aria-labelledbyandaria-describedbypoint at fixed ids generated once perDialog, wired toDialogTitleandDialogDescriptionrespectively. Always render aDialogTitle— it's the dialog's accessible name.DialogDescriptionis optional, but thearia-describedbyreference exists whether or not you render one.- Keyboard:
Escapecloses the dialog through the native<dialog>cancel/close event — genuine browser behavior, not a custom keydown handler.Tab/Shift+Tabare trapped inside the dialog automatically by the browser's own modal focus management while it's open. - Initial focus is also native
showModal()behavior — the browser focuses the first focusable element inside the dialog (or the dialog itself if there isn't one) the moment it opens. Nothing in this component manages focus manually. - Clicking the backdrop closes the dialog: a click handler compares
event.targetagainst the<dialog>element itself (not.ds-dialog__inner), since a native<dialog>'s backdrop region is the element's own box outside its content. DialogTriggerandDialogCloseare both plain<button type="button">s with no extra keyboard behavior beyond the native button contract (Enter/Spaceactivate).
Do
- Always render a DialogTitle — its id is hardcoded into aria-labelledby.
- Use DialogContent's native modality instead of layering your own overlay div on top of it.
- Compose DialogFooter with DialogClose plus your primary action for a clear cancel/confirm pair.
- Let the browser manage the focus trap and Escape — no extra keydown handler is needed for either.
Don't
- Render DialogContent outside a Dialog provider — it throws.
- Skip DialogDescription when the dialog needs supporting context — aria-describedby points at a fixed id whether you render one or not.
- Nest a second <dialog> inside — CommandPalette is the example of doing this right: one native modal at a time.
- Use Dialog for lightweight, non-blocking content — reach for Popover when nothing needs to interrupt the page.