Skip to content

Dialog — spec

Synthesized from Ant Design Modal + MUI Dialog. The flagship modal primitive — focus-trapped, esc-closeable, scroll-locked overlay containing structured content (title, body, actions). Used for confirmations, focused tasks, and content too important to dismiss inline.

When to use

  • Confirmations with destructive consequences ("Delete?", "Leave without saving?").
  • Focused short tasks that pull the user out of context (invite, schedule, single-step config).
  • Critical disclosures the user must acknowledge.

When NOT to use

  • Long forms (5+ fields) → use a dedicated page.
  • Optional info → use a Tooltip / Popover / Sheet.
  • Errors that don't block flow → use Toast / Snackbar.

Anatomy

┌─[scrim]─────────────────────────────────────┐
│                                             │
│   ┌──────────────────────────────────┐      │
│   │ DialogTitle              [✕]     │      │
│   ├──────────────────────────────────┤      │
│   │ DialogContent                    │      │
│   │   DialogContentText (optional)   │      │
│   │   form fields / list / image     │      │
│   ├──────────────────────────────────┤      │
│   │           [Cancel] [Primary]     │      │
│   └──────────────────────────────────┘      │
│                                             │
└─────────────────────────────────────────────┘

API

<Dialog
  open={open}
  onClose={handleClose}
  fullWidth
  maxWidth="sm"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc"
>
  <DialogTitle id="dialog-title">팀원 초대</DialogTitle>
  <DialogContent>
    <DialogContentText id="dialog-desc">
      이메일로 초대 링크를 보내요.
    </DialogContentText>
    <TextField autoFocus fullWidth label="이메일" />
  </DialogContent>
  <DialogActions>
    <Button onClick={handleClose}>취소</Button>
    <Button onClick={handleSend} variant="contained">보내기</Button>
  </DialogActions>
</Dialog>
Prop Type Default Description
open boolean required Controlled open state
onClose (e, reason) => void Called on backdrop click / Esc / close button
fullWidth boolean false Stretch to maxWidth
maxWidth 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| false 'sm' Width cap
fullScreen boolean false Full viewport (mobile)
scroll 'paper' \| 'body' 'paper' paper: content scrolls inside; body: page scrolls
disableEscapeKeyDown boolean false Block Esc — use sparingly
aria-labelledby string Required; matches DialogTitle's id
aria-describedby string Required when there's body description; matches DialogContentText's id
TransitionComponent Component Fade Custom enter/exit transition
keepMounted boolean false Keep DOM after close (state preservation)

States

State Visual
Closed Not in DOM (or display: none if keepMounted)
Opening Scrim fades in (200ms); dialog scales/fades from 90% to 100%
Open Focus trapped inside; scroll locked on body; backdrop receives Esc
Closing Reverse of opening; restore focus to opener

Tokens consumed

--scrim-bg              /* black/40 light, black/60 dark */
--dialog-bg             /* surface elevated */
--dialog-radius
--dialog-shadow         /* elevation-md */
--dialog-max-width-sm   /* 444 */
--dialog-max-width-md   /* 600 */
--dialog-max-width-lg   /* 900 */
--space-md              /* internal padding */
--motion-duration-200
--motion-easing-out

Accessibility

  • Focus trap: Tab cycles within dialog only. First focus goes to first interactive element (or autoFocus button).
  • Escape closes (unless disableEscapeKeyDown).
  • Scroll lock: body scroll disabled while dialog open.
  • role="dialog" + aria-modal="true" (MUI sets these).
  • aria-labelledby + aria-describedby MUST be set; otherwise screen readers announce just "dialog".
  • On close: focus returns to the element that opened the dialog.
  • Cite knowledge/a11y/keyboard-and-focus.md.

Edge cases

  • Backdrop click ambiguityonClose fires with reason='backdropClick'. For destructive flows (unsaved changes), confirm before actually closing.
  • Nested dialogs — possible but discouraged. If unavoidable: focus trap moves to the inner one, restores correctly on close.
  • Mobile fullscreen — set fullScreen={isMobile} (e.g., useMediaQuery('(max-width:600px)')) so dialogs fill the small viewport instead of cramping.
  • Korean honorific — title 합쇼체 for confirmations ("삭제하시겠습니까?"); 해요체 for friendly modals ("초대 보내볼까요?"). Cite knowledge/i18n/korean-product-conventions.md.
  • Long content — set scroll="paper" (default) and pair dividers on DialogContent to signal scrollability.

Code example

function DeleteConfirmDialog({ open, onClose, onConfirm, isPending }) {
  return (
    <Dialog
      open={open}
      onClose={(_, reason) => {
        if (reason === 'backdropClick' && isPending) return; // block during pending
        onClose();
      }}
      aria-labelledby="del-title"
      aria-describedby="del-desc"
      maxWidth="sm"
      fullWidth
    >
      <DialogTitle id="del-title">정말 삭제할까요?</DialogTitle>
      <DialogContent>
        <DialogContentText id="del-desc">
           작업은 되돌릴  없어요.
        </DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose}>취소</Button>
        <LoadingButton
          onClick={onConfirm}
          loading={isPending}
          color="error"
          variant="contained"
        >
          삭제
        </LoadingButton>
      </DialogActions>
    </Dialog>
  );
}

Don't

  • Don't disable Esc unless the flow truly requires explicit confirmation.
  • Don't put 4+ buttons in DialogActions — overflow into a menu or split into steps.
  • Don't open a dialog from a dialog (nested) without strong reason.
  • Don't make the dialog dismissable during a destructive operation that's actually running.
  • Don't omit aria-labelledby — the dialog has no accessible name without it.

References

  • Ant Design: Modal — class API, mask
  • MUI: Dialog — flagship reference

Cross-reference