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>
Related¶
- Hooks — Modal hooks
- Forms — Form patterns
- State Management — Dialog state