Skip to content

Dialog Patterns

Consistent modal and dialog patterns across the application.

Overview

flowchart TB
    subgraph Dialogs
        A[ConfirmDeleteDialog]
        B[FormModal]
        C[UnsavedChangesDialog]
    end

    subgraph System
        D[PrivacyNoticeDialog]
        E[PwaUpdateDialog]
    end

    subgraph Hooks
        F[useDeleteConfirmation]
        G[useEntityModal]
        H[useGuardedModal]
    end

    F --> A
    G --> B
    H --> B
    H --> C

Confirmation Dialog

For destructive actions requiring user confirmation. Use with useDeleteConfirmation hook for clean state management.

const deleteConfirmation = useDeleteConfirmation();
const deleteMutation = useDeleteItem();

return (
  <>
    <ItemList onDelete={deleteConfirmation.openDelete} />
    <ConfirmDeleteDialog
      open={deleteConfirmation.isOpen}
      onConfirm={() => deleteConfirmation.confirmDelete(deleteMutation.mutate)}
      onCancel={deleteConfirmation.cancelDelete}
      isLoading={deleteMutation.isPending}
    />
  </>
);

Pattern: - useDeleteConfirmation manages which item is pending deletion - confirmDelete passes the deletingId to the mutation callback - Automatic cleanup after confirm or cancel - Loading state during async action

Form Dialog

Dialog containing a form with validation. Use the FormModal component for entity create/edit dialogs:

<FormModal<FormData, Entity>
  open={open}
  onClose={onClose}
  onSubmit={handleSubmit}
  createSchema={createSchema}
  defaultValues={defaultValues}
  entity={editingEntity}
  mapEntityToForm={mapEntityToForm}
>
  {({ control, errors, isEditMode }) => (
    <Controller name="name" control={control} render={...} />
  )}
</FormModal>

Features: - Dirty state tracking - Unsaved changes warning on all close paths (via useGuardedModal internally) - Form reset on close - Loading state during submission

Entity Dialog

Full CRUD dialog for entity management.

const { isOpen, entity, openCreate, openEdit, close } = useEntityModal<Category>();

<EntityDialog
  open={isOpen}
  entity={entity}
  onSave={handleSave}
  onDelete={handleDelete}
  onClose={close}
/>

Unsaved Changes Dialog

Warns users before discarding form changes. Driven by useGuardedModal, which guards every close path of the modal:

const { handleClose, dialogProps } = useGuardedModal({
  id: "marker-form",
  open,
  isDirty: isDirty && !isSubmitting,
  onClose: () => {
    reset();
    onClose();
  },
});

return (
  <>
    <Dialog open={open} onClose={handleClose}>...</Dialog>
    <UnsavedChangesDialog {...dialogProps} />
  </>
);

Guarded close paths: - Close button / cancel action (handleClose) - Backdrop click and Escape (Dialog onClose) - Browser back button (internal modal stack registration)

Privacy Notice Dialog

First-run privacy consent.

const { hasAccepted, accept } = usePrivacyNoticeStore();

{!hasAccepted && (
  <PrivacyNoticeDialog
    open={true}
    onAccept={accept}
  />
)}

Persisted in localStorage.

PWA Update Dialog

Notifies users of available updates.

const { needsUpdate, update } = usePwaUpdate();

<PwaUpdateDialog
  open={needsUpdate}
  onUpdate={update}
  onDismiss={dismiss}
/>

Dialog Best Practices

Do Don't
Use clear action verbs Use vague "OK/Cancel"
Show loading states Allow double-submission
Trap focus inside dialog Allow background interaction
Support Escape to close Require mouse to close
Animate open/close Instant show/hide

Accessibility

Dialogs must: - Trap focus within the dialog - Return focus on close - Support Escape key - Have proper ARIA attributes

MUI Dialog handles this automatically:

<Dialog
  open={open}
  onClose={onClose}
  aria-labelledby="dialog-title"
>
  <DialogTitle id="dialog-title">Title</DialogTitle>
  ...
</Dialog>