Dialog
A composable, accessible modal dialog with built-in focus trapping, Escape key handling, backdrop clicks, and full ARIA wiring. Style the overlay, panel, and sub-components freely with Tailwind utilities. Zero JavaScript required — focus management is pure C#.
Overview
MDialog manages open/close state, focus, Escape key handling, and backdrop clicks entirely
in C#. Sub-components — MDialogHeader, MDialogTitle,
MDialogDescription, MDialogContent, MDialogFooter, and MDialogClose
— render their semantic elements. Pass Class to each to provide layout and visual styles.
Usage
Add the namespace import to use the component.
@using Marai.UI.Components.MDialogBasic Usage
Supply OverlayClass for the backdrop wrapper and Class for the dialog panel. Then style each sub-component with its own Class:
<MButton Class="inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800" @onclick="() => isOpen = true">Open Dialog</MButton>
<MDialog @bind-IsOpen="isOpen"
OverlayClass="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
Class="relative w-full max-w-lg rounded-lg border border-slate-200 bg-white shadow-lg">
<MDialogHeader Class="flex flex-col gap-1.5 p-6 pb-2">
<MDialogTitle Class="text-lg font-semibold leading-none text-slate-900">Are you sure?</MDialogTitle>
<MDialogDescription Class="text-sm text-slate-500">This action cannot be undone.</MDialogDescription>
</MDialogHeader>
<MDialogContent Class="px-6 py-4">
<p class="text-sm text-slate-700">Message HERE</p>
</MDialogContent>
<MDialogFooter Class="flex justify-end gap-2 px-6 pb-6 pt-2">
<MButton Class="inline-flex items-center justify-center rounded-md border border-red-600 px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-50" @onclick="() => isOpen = false">Cancel</MButton>
<MButton Class="inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" @onclick="() => isOpen = false">Confirm</MButton>
</MDialogFooter>
</MDialog>
@code {
private bool isOpen;
}
Composition
Mix and match dialog sub-components to create different layouts:
Confirmation Dialog
Use for destructive actions that require explicit user confirmation:
<MButton Class="inline-flex items-center justify-center rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700" @onclick="() => isOpen = true">Delete Account</MButton>
<MDialog @bind-IsOpen="isOpen"
OverlayClass="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
Class="relative w-full max-w-lg rounded-lg border border-slate-200 bg-white shadow-lg">
<MDialogHeader Class="flex flex-col gap-1.5 p-6 pb-2">
<MDialogTitle Class="text-lg font-semibold leading-none text-slate-900">Delete Account</MDialogTitle>
</MDialogHeader>
<MDialogContent Class="px-6 py-4 text-sm text-slate-700">
This will permanently delete your account and all associated data.
This action cannot be undone.
</MDialogContent>
<MDialogFooter Class="flex justify-end gap-2 px-6 pb-6 pt-2">
<MButton Class="inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50" @onclick="() => isOpen = false">Cancel</MButton>
<MButton Class="inline-flex items-center justify-center rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700" @onclick="() => isOpen = false">Delete</MButton>
</MDialogFooter>
</MDialog>
@code {
private bool isOpen;
}
Form Dialog
Use for collecting user input within a modal context:
<MButton Class="inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800" @onclick="() => isOpen = true">Edit Profile</MButton>
<MDialog @bind-IsOpen="isOpen"
OverlayClass="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
Class="relative w-full max-w-lg rounded-lg border border-slate-200 bg-white shadow-lg">
<MDialogHeader Class="flex flex-col gap-1.5 p-6 pb-2">
<MDialogTitle Class="text-lg font-semibold leading-none text-slate-900">Edit Profile</MDialogTitle>
<MDialogDescription Class="text-sm text-slate-500">Update your display name and email address.</MDialogDescription>
</MDialogHeader>
<MDialogContent Class="grid gap-4 px-6 py-4">
<div>
<MLabel For="name">Display Name</MLabel>
<MInput id="name" Placeholder="Your name" />
</div>
<div>
<MLabel For="email">Email</MLabel>
<MInput id="email" Type="email" Placeholder="you@example.com" />
</div>
</MDialogContent>
<MDialogFooter Class="flex justify-end gap-2 px-6 pb-6 pt-2">
<MButton Class="inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50" @onclick="() => isOpen = false">Cancel</MButton>
<MButton Class="inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" @onclick="() => isOpen = false">Save Changes</MButton>
</MDialogFooter>
</MDialog>
@code {
private bool isOpen;
}
Content Only
Simple dialogs without headers or footers:
<MButton Class="inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800" @onclick="() => isOpen = true">Simple Dialog</MButton>
<MDialog @bind-IsOpen="isOpen"
OverlayClass="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
Class="relative w-full max-w-lg rounded-lg border border-slate-200 bg-white shadow-lg">
<MDialogContent Class="p-6">
<p class="text-sm text-slate-700">This dialog has no header or footer — just content.</p>
</MDialogContent>
</MDialog>
@code {
private bool isOpen;
}
Focus Management
MDialog manages focus automatically via a lightweight JS focus trap.
When a dialog opens, focus moves to the first focusable element inside the panel (or the panel itself).
When the dialog closes, focus returns to the element that triggered it.
Tab and Shift+Tab are trapped inside the open dialog so keyboard-only users cannot accidentally navigate behind the overlay.
<MButton Class="inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800" @onclick="() => isOpen = true">Test Focus Management</MButton>
<MDialog @bind-IsOpen="isOpen"
OverlayClass="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
Class="relative w-full max-w-lg rounded-lg border border-slate-200 bg-white shadow-lg">
<MDialogHeader Class="flex flex-col gap-1.5 p-6 pb-2">
<MDialogTitle Class="text-lg font-semibold leading-none text-slate-900">Focus Management Test</MDialogTitle>
<MDialogDescription Class="text-sm text-slate-500">Testing pure C# focus handling</MDialogDescription>
</MDialogHeader>
<MDialogContent Class="grid gap-4 px-6 py-4">
<p class="text-sm text-slate-700">When this dialog opens, focus is automatically moved to the dialog panel using <code>FocusAsync()</code>.</p>
<div>
<MLabel For="input1">First Input</MLabel>
<MInput id="input1" Placeholder="Tab to navigate" />
</div>
<div>
<MLabel For="input2">Second Input</MLabel>
<MInput id="input2" Placeholder="Press Escape to close" />
</div>
</MDialogContent>
<MDialogFooter Class="flex justify-end gap-2 px-6 pb-6 pt-2">
<MButton Class="inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50" @onclick="() => isOpen = false">Cancel</MButton>
<MButton Class="inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800" @onclick="() => isOpen = false">OK</MButton>
</MDialogFooter>
</MDialog>
@code {
private bool isOpen;
}
- Tab — Move focus to next interactive element within dialog
- Shift + Tab — Move focus to previous interactive element
- Escape — Close the dialog
- Backdrop Click — Close the dialog
Accessibility
- Dialog panel renders with
role="dialog",aria-modal="true",aria-labelledby, andaria-describedby— wired automatically toMDialogTitleandMDialogDescriptionvia generated IDs MDialogTitleandMDialogDescriptionreceive stable generatedidattributes; override withId="your-id"when needed- Tab and Shift+Tab are trapped inside the open dialog — keyboard users cannot reach content behind the overlay
- Focus moves to the first focusable element on open; returns to the trigger element on close
- Escape closes the dialog from anywhere inside it
- Dialog panel has
tabindex="-1"so it can receive focus as a fallback when no focusable children are present - Screen readers announce the dialog title and description automatically via
aria-labelledby/aria-describedby
| Attribute | Applied to | Value |
|---|---|---|
role="dialog" | Panel | Static |
aria-modal="true" | Panel | Static |
aria-labelledby | Panel | Generated ID matching MDialogTitle |
aria-describedby | Panel | Generated ID matching MDialogDescription |
API Reference
MDialog
| Parameter | Type | Default | Description |
|---|---|---|---|
| IsOpen | bool | false |
Controls whether the dialog is visible. Use @bind-IsOpen for two-way binding. |
| IsOpenChanged | EventCallback<bool> | — | Callback invoked when the dialog requests close. |
| OverlayClass | string? | null |
CSS classes applied to the full-screen overlay wrapper div. |
| Class | string? | null |
CSS classes applied to the dialog panel element. |
| ChildContent | RenderFragment? | null |
Slot content — typically composed of dialog sub-components. |
| AdditionalAttributes | Dictionary<string, object>? | null |
Arbitrary HTML attributes passed to the dialog panel element. |
Sub-Components
All sub-components share these common parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
| ChildContent | RenderFragment? | null |
Content rendered inside the component. |
| Class | string? | null |
CSS classes applied to the root element. No default styles are added. |
| AdditionalAttributes | Dictionary<string, object>? | null |
Arbitrary HTML attributes passed through. |
- Always include a
MDialogTitlefor accessibility - Use
MDialogDescriptionto provide context about the dialog's purpose - Place action buttons in
MDialogFooterfor consistent positioning - Use
MDialogCloseto compose your own close control — it reads the cascaded context automatically - Ensure destructive actions (like Delete) have clear confirmation dialogs
- Keep dialog content focused — avoid cramming too much information
- Test keyboard navigation (Tab, Escape) thoroughly