Dialog

Modal dialog built on the native dialog element.

organismsSource
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.

Contact Eli

Practical AI consulting and tech support. No contracts required.

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.

Delete "U8 Soccer Season Plan"?

This removes the guide from your library. Anyone who already downloaded it keeps their copy.

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 &quot;U8 Soccer Season Plan&quot;?</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

PropTypeDefaultDescription
defaultOpenbooleanfalse
onOpenChange((open: boolean) => void)
openboolean

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 via showModal()/close() — not a custom-built modal. The browser supplies the modal semantics, the focus trap, and the top-layer stacking.
  • aria-labelledby and aria-describedby point at fixed ids generated once per Dialog, wired to DialogTitle and DialogDescription respectively. Always render a DialogTitle — it's the dialog's accessible name. DialogDescription is optional, but the aria-describedby reference exists whether or not you render one.
  • Keyboard: Escape closes the dialog through the native <dialog> cancel/close event — genuine browser behavior, not a custom keydown handler. Tab/Shift+Tab are 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.target against the <dialog> element itself (not .ds-dialog__inner), since a native <dialog>'s backdrop region is the element's own box outside its content.
  • DialogTrigger and DialogClose are both plain <button type="button">s with no extra keyboard behavior beyond the native button contract (Enter/Space activate).

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.