M
Marai.UI
v1.0.0-alpha.7

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.

razor
@using Marai.UI.Components.MDialog

Basic Usage

Supply OverlayClass for the backdrop wrapper and Class for the dialog panel. Then style each sub-component with its own Class:

Preview
razor
<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:

Preview
razor
<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:

Preview
razor
<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:

Preview
razor
<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.

Preview
razor
<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;
}
Keyboard Interactions
  • 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, and aria-describedby — wired automatically to MDialogTitle and MDialogDescription via generated IDs
  • MDialogTitle and MDialogDescription receive stable generated id attributes; override with Id="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
ARIA Attributes (auto-generated)
AttributeApplied toValue
role="dialog"PanelStatic
aria-modal="true"PanelStatic
aria-labelledbyPanelGenerated ID matching MDialogTitle
aria-describedbyPanelGenerated 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.
Best Practices
  • Always include a MDialogTitle for accessibility
  • Use MDialogDescription to provide context about the dialog's purpose
  • Place action buttons in MDialogFooter for consistent positioning
  • Use MDialogClose to 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