Skip to content

Documentation

neelam-ui

Search documentation

Getting Started
Forms
Overlays
Navigation
Data Display
Layout
AI & Chat
Blocks
GitHub repository

Dialog

A modal window layered over the page, built on the native <dialog> element — so focus trapping, Escape-to-close, and top-layer stacking come from the browser.

Edit profile

Make changes to your profile here. Click save when you're done.

Usage#

import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "neelam-ui";
 
<Dialog>
  <DialogTrigger>Open</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Edit profile</DialogTitle>
      <DialogDescription>Make changes to your profile.</DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <DialogClose>Cancel</DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>

Why the native element matters#

DialogContent is a real <dialog> opened with showModal(). That single choice hands four hard problems to the browser:

  • Focus trappingTab cannot leave the dialog, with no key interception.
  • Inert background — content behind is made inert by the platform, so it is removed from the accessibility tree, not merely visually covered.
  • Top-layer stacking — the dialog renders above everything regardless of z-index or transformed ancestors, which is where hand-rolled modals usually break.
  • Escape to close — built in, including the cancel event.

A dialog is one of the few widgets where a native element genuinely beats a JavaScript implementation. Most focus-trap bugs in the wild come from re-implementing what showModal() already does correctly.

Destructive confirmation#

Are you absolutely sure?

This action cannot be undone. This will permanently delete your account and remove your data from our servers.

Consider AlertDialog instead

For an interruption the user must resolve before continuing — a genuinely irreversible action — AlertDialog carries role="alertdialog" and does not close on an outside click, which is the correct pattern there.

Controlled#

Pass open and onOpenChange to drive the dialog from your own state — for closing it after an async save completes, for example:

const [open, setOpen] = useState(false);
 
<Dialog open={open} onOpenChange={setOpen}>
  <DialogContent>…</DialogContent>
</Dialog>

onOpenChange fires for every path that changes the state: DialogTrigger, DialogClose, the built-in close button, Escape, and an outside click.

Driving the dialog from inside#

useDialog gives any descendant access to the open state, so a control that is not a DialogClose can still close the dialog:

import { Button, useDialog } from "neelam-ui";
 
function SaveButton() {
  const { onOpenChange } = useDialog();
  return <Button onClick={() => onOpenChange(false)}>Save changes</Button>;
}

This is the recommended way to get a real Button into a dialog footer — DialogClose renders its own element and only closes.

Keyboard#

Keyboard shortcuts
KeyBehaviour
EnterSpaceOpens the dialog when focus is on DialogTrigger.
EscapeCloses the dialog. Native <dialog> behaviour.
TabShift+TabCycles focus within the dialog and cannot leave it.

Accessibility#

  • DialogTitle provides the dialog's accessible name and is required — omit it and screen reader users get an unnamed modal.
  • DialogDescription, when present, is wired to the dialog with aria-describedby.
  • Focus moves into the dialog on open and returns to the trigger on close.
  • Background content is inert via the platform, so it cannot be reached by keyboard or virtual cursor.
  • All transitions are dropped under prefers-reduced-motion.

A title is not optional

If a visible heading does not suit the design, render DialogTitle with sr-only rather than leaving it out — that is exactly what CommandDialog does internally.

API reference#

Dialog#

Props for Dialog
PropTypeDefault
childrenReactNode
defaultOpen

Initial open state when uncontrolled. Defaults to `false`.

booleanfalse
onOpenChange

Called whenever the open state changes, whether from `DialogTrigger`, `DialogClose`, the built-in close button, Escape, or an outside click.

((open: boolean) => void)
open

Controls the open state. Omit to let the dialog manage its own state.

boolean

DialogContent#

Props for DialogContent
PropTypeDefault
closeOnOutsideClick

Closes the dialog when the area outside it (the backdrop) is clicked. Defaults to `true`.

booleantrue
hideCloseButton

Hides the built-in close button in the top-right corner. Defaults to `false`.

booleanfalse
onClose((event: SyntheticEvent<HTMLDialogElement, Event>) => void)