Dialog
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
Demo
Nested Dialogs Demo
Dialogs can be nested within each other. Each nested dialog stacks properly with correct z-index handling.
Dialog with AlertDialog Demo
Use AlertDialog for destructive actions within a Dialog. The AlertDialog appears on top of the Dialog with proper z-index stacking.
Features
- Controlled and uncontrolled modes
- Nested dialog support with depth tracking
- Automatic focus trapping and management
- Body scroll locking with iOS Safari support
- Keyboard navigation (Escape key support)
- Portal rendering for z-index control
- Close animation support
- WCAG compliant with proper ARIA attributes
Installation
dotnet add package SummitUIAnatomy
Import the components and structure them as follows:
<SmDialogRoot>
<SmDialogTrigger>Open Dialog</SmDialogTrigger>
<SmDialogPortal>
<SmDialogOverlay />
<SmDialogContent>
<SmDialogTitle>Title</SmDialogTitle>
<SmDialogDescription>Description</SmDialogDescription>
<SmDialogClose>Close</SmDialogClose>
</SmDialogContent>
</SmDialogPortal>
</SmDialogRoot>Sub-components
DialogRoot
Root component managing dialog state and providing context to children.
DialogContent
Main dialog panel with focus trapping and scroll locking.
DialogTitle
Accessible title component with auto-generated ID.
DialogDescription
Accessible description component with auto-generated ID.
DialogTrigger
Button element that opens the dialog when clicked.
DialogClose
Button element that closes the dialog when clicked.
DialogOverlay
Backdrop overlay that can close the dialog when clicked.
DialogPortal
Fixed-position container for rendering outside the DOM hierarchy.
API Reference
DialogRoot
| Property | Type | Default | Description |
|---|---|---|---|
| Open | bool? | null | Controlled open state (null for uncontrolled mode) |
| DefaultOpen | bool | false | Default open state for uncontrolled mode |
| OpenChanged | EventCallback<bool> | - | Callback on open state change |
| OnOpen | EventCallback | - | Callback when dialog opens |
| OnClose | EventCallback | - | Callback fired immediately when close is triggered (before animations start) |
| ChildContentrequired | RenderFragment | - | Child components |
DialogContent
| Property | Type | Default | Description |
|---|---|---|---|
| ChildContentrequired | RenderFragment | - | Dialog content |
| As | string | "div" | HTML element type |
| TrapFocus | bool | true | Enable focus trapping |
| PreventScroll | bool | true | Lock body scroll |
| EscapeKeyBehavior | EscapeKeyBehavior | Close | Escape key behavior (Close or Ignore) |
| OutsideClickBehavior | OutsideClickBehavior | Close | Click outside behavior (Close or Ignore) |
| OnInteractOutside | EventCallback<MouseEventArgs> | - | Callback on click outside |
| OnEscapeKeyDown | EventCallback<KeyboardEventArgs> | - | Callback on escape key |
| OnOpenAutoFocus | EventCallback | - | Callback fired after dialog opens and receives focus |
| OnCloseAutoFocus | EventCallback | - | Callback fired after close animations complete and focus returns to trigger. Use this instead of OnClose when you need to run code after the dialog is fully closed. |
| AdditionalAttributes | IDictionary<string, object> | - | Additional HTML attributes |
DialogTitle
| Property | Type | Default | Description |
|---|---|---|---|
| ChildContentrequired | RenderFragment | - | Title text |
| As | string | "h2" | HTML element type |
| AdditionalAttributes | IDictionary<string, object> | - | Additional HTML attributes |
DialogDescription
| Property | Type | Default | Description |
|---|---|---|---|
| ChildContentrequired | RenderFragment | - | Description text |
| As | string | "p" | HTML element type |
| AdditionalAttributes | IDictionary<string, object> | - | Additional HTML attributes |
DialogTrigger
| Property | Type | Default | Description |
|---|---|---|---|
| ChildContentrequired | RenderFragment | - | Trigger button content |
| As | string | "button" | HTML element type |
| AdditionalAttributes | IDictionary<string, object> | - | Additional HTML attributes |
DialogClose
| Property | Type | Default | Description |
|---|---|---|---|
| ChildContentrequired | RenderFragment | - | Close button content |
| As | string | "button" | HTML element type |
| AriaLabel | string? | "Close dialog" | Accessible label |
| AdditionalAttributes | IDictionary<string, object> | - | Additional HTML attributes |
DialogOverlay
| Property | Type | Default | Description |
|---|---|---|---|
| ChildContent | RenderFragment? | - | Optional overlay content |
| As | string | "div" | HTML element type |
| OnClick | EventCallback<MouseEventArgs> | - | Callback on click |
| CloseOnClick | bool | true | Whether clicking closes dialog |
| AdditionalAttributes | IDictionary<string, object> | - | Additional HTML attributes |
DialogPortal
| Property | Type | Default | Description |
|---|---|---|---|
| ChildContentrequired | RenderFragment | - | Portal content |
| ContainerId | string? | null | Optional custom container ID |
Examples
Basic Usage
<SmDialogRoot>
<SmDialogTrigger class="btn btn-primary">Open Dialog</SmDialogTrigger>
<SmDialogPortal>
<SmDialogOverlay class="dialog-overlay">
<SmDialogContent class="dialog-content">
<SmDialogTitle>Edit Profile</SmDialogTitle>
<SmDialogDescription>
Make changes to your profile here. Click save when you're done.
</SmDialogDescription>
<div class="dialog-actions">
<SmDialogClose class="btn btn-secondary">Cancel</SmDialogClose>
<SmDialogClose class="btn btn-primary">Save Changes</SmDialogClose>
</div>
</SmDialogContent>
</SmDialogOverlay>
</SmDialogPortal>
</SmDialogRoot>Controlled Mode
Control the dialog open state externally.
@code {
private bool isOpen = false;
}
<button @onclick="@(() => isOpen = true)">Open Dialog</button>
<SmDialogRoot Open="@isOpen" OpenChanged="@(v => isOpen = v)">
<SmDialogPortal>
<SmDialogOverlay class="dialog-overlay">
<SmDialogContent class="dialog-content">
<SmDialogTitle>Controlled Dialog</SmDialogTitle>
<SmDialogDescription>
This dialog's state is managed externally.
</SmDialogDescription>
<SmDialogClose class="btn btn-primary">Close</SmDialogClose>
</SmDialogContent>
</SmDialogOverlay>
</SmDialogPortal>
</SmDialogRoot>Nested Dialogs
Dialogs can be nested within each other. Depth is tracked via CSS variables.
<SmDialogRoot>
<SmDialogTrigger class="btn btn-primary">Open Parent</SmDialogTrigger>
<SmDialogPortal>
<SmDialogOverlay class="dialog-overlay">
<SmDialogContent class="dialog-content">
<SmDialogTitle>Parent Dialog</SmDialogTitle>
<SmDialogDescription>This is the parent dialog.</SmDialogDescription>
<SmDialogRoot>
<SmDialogTrigger class="btn btn-secondary">Open Nested</SmDialogTrigger>
<SmDialogPortal>
<SmDialogOverlay class="dialog-overlay nested">
<SmDialogContent class="dialog-content nested">
<SmDialogTitle>Nested Dialog</SmDialogTitle>
<SmDialogDescription>This is nested inside another dialog.</SmDialogDescription>
<SmDialogClose class="btn btn-secondary">Close</SmDialogClose>
</SmDialogContent>
</SmDialogOverlay>
</SmDialogPortal>
</SmDialogRoot>
<SmDialogClose class="btn btn-secondary">Close Parent</SmDialogClose>
</SmDialogContent>
</SmDialogOverlay>
</SmDialogPortal>
</SmDialogRoot>Dialog with Form
A dialog containing a form with multiple focusable elements.
<SmDialogRoot>
<SmDialogTrigger class="btn btn-primary">Edit Profile</SmDialogTrigger>
<SmDialogPortal>
<SmDialogOverlay class="dialog-overlay">
<SmDialogContent class="dialog-content">
<SmDialogTitle>Edit Profile</SmDialogTitle>
<SmDialogDescription>
Make changes to your profile here. Click save when you're done.
</SmDialogDescription>
<EditForm Model="@model" OnValidSubmit="HandleSubmit">
<div class="dialog-form">
<div class="form-group">
<label for="name">Name</label>
<InputText id="name" class="form-control" @bind-Value="model.Name" />
</div>
<div class="form-group">
<label for="email">Email</label>
<InputText id="email" class="form-control" @bind-Value="model.Email" />
</div>
</div>
<div class="dialog-actions">
<SmDialogClose type="submit" class="btn btn-primary">Save Changes</SmDialogClose>
<SmDialogClose class="btn btn-secondary">Cancel</SmDialogClose>
</div>
</EditForm>
</SmDialogContent>
</SmDialogOverlay>
</SmDialogPortal>
</SmDialogRoot>Dialog with AlertDialog
Trigger an AlertDialog confirmation from within a Dialog for destructive actions.
@inject IAlertDialogService AlertDialogService
<SmDialogRoot>
<SmDialogTrigger class="btn btn-primary">Edit Settings</SmDialogTrigger>
<SmDialogPortal>
<SmDialogOverlay class="dialog-overlay">
<SmDialogContent class="dialog-content">
<SmDialogTitle>Settings</SmDialogTitle>
<SmDialogDescription>
Manage your account settings and preferences.
</SmDialogDescription>
<div class="dialog-form">
<!-- Settings form fields here -->
</div>
<div class="danger-zone">
<h4>Danger Zone</h4>
<button @onclick="ShowDeleteConfirmation" class="btn btn-destructive">
Delete Account
</button>
@if (!string.IsNullOrEmpty(deleteResult))
{
<p>@deleteResult</p>
}
</div>
<div class="dialog-actions">
<SmDialogClose class="btn btn-secondary">Close</SmDialogClose>
<SmDialogClose class="btn btn-primary">Save Changes</SmDialogClose>
</div>
</SmDialogContent>
</SmDialogOverlay>
</SmDialogPortal>
</SmDialogRoot>
@code {
private string? deleteResult;
private async Task ShowDeleteConfirmation()
{
var confirmed = await AlertDialogService.ConfirmAsync(
"This will permanently delete your account. This action cannot be undone.",
new AlertDialogOptions
{
Title = "Delete Account",
ConfirmText = "Delete",
CancelText = "Cancel",
IsDestructive = true
});
deleteResult = confirmed ? "Account deleted!" : "Deletion cancelled.";
}
}Callback Timing with Animations
The dialog fires callbacks at different points during the open/close sequence. Understanding when each callback fires is important for proper integration.
| Callback | Location | When it fires |
|---|---|---|
| OnClose | DialogRoot | Immediately when close is triggered (before animations start) |
| OnCloseAutoFocus | DialogContent | After all close animations complete and focus returns to trigger |
When to use each
- OnClose: Use for immediate state updates, analytics, or when animation timing doesn't matter
- OnCloseAutoFocus: Use when you need to ensure the dialog is fully closed (e.g., cleanup, navigation, or showing a toast after the dialog disappears)
Example
<SmDialogRoot OnClose="HandleClose">
<SmDialogTrigger class="btn btn-primary">Open Dialog</SmDialogTrigger>
<SmDialogPortal>
<SmDialogOverlay class="dialog-overlay" />
<SmDialogContent class="dialog-content" OnCloseAutoFocus="HandleCloseComplete">
<SmDialogTitle>Example Dialog</SmDialogTitle>
<SmDialogDescription>This dialog demonstrates callback timing.</SmDialogDescription>
<SmDialogClose class="btn btn-primary">Close</SmDialogClose>
</SmDialogContent>
</SmDialogPortal>
</SmDialogRoot>
@code {
void HandleClose()
{
// Fires immediately when close is triggered
// Animations are still running at this point
Console.WriteLine("Close triggered - animations starting");
}
void HandleCloseComplete()
{
// Fires after all animations complete
// Dialog is now fully closed and focus has returned to trigger
Console.WriteLine("Dialog fully closed - safe to navigate or show toast");
}
}Styling
Data Attributes
| Attribute | Values | Description |
|---|---|---|
| data-state | "open" | "closed" | Dialog open state |
| data-nested | Present on nested dialogs | Indicates dialog is nested |
| data-nested-open | Number of open nested dialogs | Count of nested dialogs that are open |
CSS Variables
| Variable | Description |
|---|---|
| --summit-dialog-depth | Nesting depth (0-based) |
| --summit-dialog-nested-count | Number of nested open dialogs |
data-[state=closed]:opacity-0 alongside your animate-out classes).
CSS Example
/* Overlay styles */
.dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.dialog-overlay[data-state="open"] {
animation: fadeIn 200ms ease-out;
}
.dialog-overlay[data-state="closed"] {
animation: fadeOut 200ms ease-in;
}
/* Content styles - using flexbox centering to avoid stacking context issues */
.dialog-content {
position: fixed;
inset: 0;
margin: auto;
width: fit-content;
height: fit-content;
background: white;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 1.5rem;
min-width: 400px;
max-width: 90vw;
max-height: 85vh;
overflow-y: auto;
z-index: 1001;
}
.dialog-content[data-state="open"] {
animation: scaleIn 200ms ease-out;
}
.dialog-content[data-state="closed"] {
animation: scaleOut 200ms ease-in;
}
/* Nested dialog styling */
.dialog-overlay[data-nested] {
background: rgba(0, 0, 0, 0.3);
}
/* Adjust z-index based on depth for proper nested stacking */
.dialog-content {
z-index: calc(1001 + var(--summit-dialog-depth, 0) * 10);
}
.dialog-overlay {
z-index: calc(1000 + var(--summit-dialog-depth, 0) * 10);
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes scaleIn {
from {
opacity: 0;
scale: 0.95;
}
to {
opacity: 1;
scale: 1;
}
}
@keyframes scaleOut {
from {
opacity: 1;
scale: 1;
}
to {
opacity: 0;
scale: 0.95;
}
}Accessibility
Keyboard Navigation
| Key | Action |
|---|---|
| Escape | Closes the dialog |
| Tab | Moves focus to next focusable element |
| Shift + Tab | Moves focus to previous focusable element |
ARIA Attributes
- DialogContent:
Has
role="dialog",aria-modal,aria-labelledby, andaria-describedby - DialogTitle:
Auto-generates ID and is referenced by content's
aria-labelledby - DialogDescription:
Auto-generates ID and is referenced by content's
aria-describedby - DialogTrigger:
Has
aria-haspopup,aria-expanded, andaria-controls - DialogOverlay:
Has
aria-hidden="true"