Dialogs
CDX dialogs are composed from five primitives: DialogShell glues them together, and ConfirmationDialog provides the four canonical flavors. Custom dialogs use the primitives directly.
Live examples
Open the full Dialogs example.
ConfirmationDialog
Four named factories cover the standard cases. Always invoked through Flutter's showDialog.
| Factory | Variant | Buttons |
|---|---|---|
.info | standard | OK |
.error | error | OK |
.confirm | primary | Cancel + Confirm |
.destructive | destructive | Cancel + Confirm (error palette) |
final result = await showDialog<bool>(
context: context,
builder: (_) => ConfirmationDialog.destructive(
title: 'Delete area?',
description: 'This action cannot be undone.',
confirmLabel: 'Delete',
onCancel: () => Navigator.of(context).pop(false),
onConfirm: () => Navigator.of(context).pop(true),
),
);
// Info — single OK button
await showDialog<void>(
context: context,
builder: (_) => ConfirmationDialog.info(
title: 'Submitted',
description: 'Your request has been submitted for approval.',
onDismiss: () => Navigator.of(context).pop(),
),
);Optional body
Both .confirm and .destructive accept a body widget rendered below the description — use it for inline forms, e-signature inputs, or additional context.
ConfirmationDialog.confirm(
title: 'Approve change',
description: 'Sign with your initials to approve.',
confirmLabel: 'Approve',
isDisabled: initials.value.isEmpty,
isLoading: store.isSubmitting,
onCancel: () => Navigator.of(context).pop(),
onConfirm: () async => await store.approve(initials.value),
body: TextField(
decoration: const InputDecoration(labelText: 'Initials'),
controller: initials,
),
)DialogShell
The base container — modal barrier, sizing, header divider, and footer auto-wiring. Use it directly when you need a custom body or footer layout.
showDialog<void>(
context: context,
builder: (_) => DialogShell(
title: 'Edit profile',
icon: FluentIcons.person_edit_24_regular,
width: 720,
maxHeight: 600,
headerActions: [Button.icon(icon: FluentIcons.history_24_regular, ...)],
footerMessage: const DialogMessage(
text: 'Changes apply immediately.',
severity: MessageSeverity.info,
),
actions: [
DialogAction.cancel(onPressed: () => Navigator.pop(context)),
DialogAction.primary(label: 'Save', onPressed: _save),
],
content: const ProfileForm(),
),
);Parameters
| Param | Type | Default |
|---|---|---|
title / titleWidget | String? / Widget? | One required |
icon / iconColor | IconData? / Color? | null |
variant | DialogVariant | standard |
content | Widget | required |
actions | List<DialogAction>? | null (no footer) |
headerActions | List<Widget>? | null |
footerMessage | DialogMessage? | null |
width | double? | 600 |
maxHeight | double? | 500 |
showCloseButton | bool? | variant default |
onClose | VoidCallback? | Navigator.pop |
DialogShell.bare
The same surface (color, radius, border, shadow, enter animation, resize and optional footer) with no header and no divider, for content that draws its own title and closes itself, such as an editor with its own header. It takes content, actions, footerMessage, width, maxHeight and resize; nothing in the dialog closes it, so the content pops the route.
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => DialogShell.bare(
width: 760,
maxHeight: 760,
content: SizedBox(height: 640, child: RecordEditor(onCancel: close)),
),
);Primitives
DialogVariant
enum DialogVariant { standard, primary, destructive, error }Drives header background and close-button visibility:
| Variant | Header bg | Default close button |
|---|---|---|
standard | surfaceContainerHighest + tinted icon circle | shown |
primary | primaryContainer + onPrimaryContainer foreground | hidden |
destructive | errorContainer + onErrorContainer | hidden |
error | errorContainer + onErrorContainer | shown |
DialogHeader
DialogHeader(
title: 'Edit area',
icon: FluentIcons.edit_24_regular,
variant: DialogVariant.primary,
onClose: () => Navigator.pop(context),
actions: [const _HistoryButton()],
leading: IconButton(icon: const Icon(FluentIcons.arrow_left_24_regular), onPressed: ...),
)leading is reserved for back-arrow affordances (used by CdxPanelPage). actions render before the close button.
DialogFooter / DialogAction
DialogFooter(
message: const DialogMessage(text: 'All fields required.', severity: MessageSeverity.warning),
actions: [
DialogAction.cancel(onPressed: () => Navigator.pop(context)),
DialogAction.primary(label: 'Save', icon: FluentIcons.save_24_regular, onPressed: _save),
],
)DialogAction factories — .cancel, .ok, .primary, .destructive. Each carries isLoading / isDisabled for primary/destructive variants.
DialogMessage / MessageSeverity
Inline message banner inside the footer. MessageSeverity.error / warning / success / info maps to the matching Container palette (errorContainer, tertiaryContainer, etc.).
Cross-links
- Buttons —
Button.iconfor header actions - Panels —
CdxPanelPagereusesDialogHeader+DialogFooter - Theming —
containerBorderRadiusfor the dialog frame
Resize behavior
DialogShell.resize takes a sealed DialogResize configuration. The default is const DialogResize.fixed(). A dialog draws no resize icon of its own: the cursor over each enabled edge or corner says it can be dragged, and each handle has a tooltip and semantics that name the edge and the axes it resizes. A fixed dialog has no handles, so its edges keep the ordinary cursor.
DialogShell(
title: 'Preview',
width: 600,
maxHeight: 500,
resize: const DialogResize.asymmetric(
axis: DialogResizeAxis.both,
handles: {DialogResizeHandle.bottomRight},
constraints: BoxConstraints(
minWidth: 400,
minHeight: 300,
maxWidth: 1000,
maxHeight: 800,
),
),
content: preview,
)DialogResize.symmetric()keeps the dialog centered; opposite sides move equally.DialogResize.asymmetric()moves only the dragged sides, anchoring the opposite edge or corner.DialogResizeAxis.horizontal,.vertical, or.bothselects which dimensions may change. A corner respects the same axis restriction.handlesaccepts a singleton set, any selected set, or the constant setsDialogResizeHandle.corners,.edges, or.all(the default). Incompatible edges are omitted—for example, top/bottom edges in horizontal-only mode. Choose at least one handle that can affect the selected axis.constraintsdefaults to a minimum of 320 × 240. The available viewport, keyboard insets, and anchored sides take precedence over configured limits.widthandmaxHeightprovide the initial size for a resizable dialog;resize.constraintsprovides its ongoing minimum/maximum dimensions.
The old resizable: true flag is replaced by resize: const DialogResize.symmetric(). Fixed dialogs may omit resize.