Dialog
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
Demo
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:
<DialogRoot>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle>Title</DialogTitle>
<DialogDescription>Description</DialogDescription>
<DialogClose>Close</DialogClose>
</DialogContent>
</DialogPortal>
</DialogRoot>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 when dialog closes |
| 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 when dialog receives auto-focus |
| OnCloseAutoFocus | EventCallback | - | Callback when dialog loses auto-focus |
| 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
<DialogRoot>
<DialogTrigger class="btn btn-primary">Open Dialog</DialogTrigger>
<DialogPortal>
<DialogOverlay class="dialog-overlay">
<DialogContent class="dialog-content">
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
<div class="dialog-actions">
<DialogClose class="btn btn-secondary">Cancel</DialogClose>
<DialogClose class="btn btn-primary">Save Changes</DialogClose>
</div>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</DialogRoot>Controlled Mode
Control the dialog open state externally.
@code {
private bool isOpen = false;
}
<button @onclick="@(() => isOpen = true)">Open Dialog</button>
<DialogRoot Open="@isOpen" OpenChanged="@(v => isOpen = v)">
<DialogPortal>
<DialogOverlay class="dialog-overlay">
<DialogContent class="dialog-content">
<DialogTitle>Controlled Dialog</DialogTitle>
<DialogDescription>
This dialog's state is managed externally.
</DialogDescription>
<DialogClose class="btn btn-primary">Close</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</DialogRoot>Nested Dialogs
Dialogs can be nested within each other. Depth is tracked via CSS variables.
<DialogRoot>
<DialogTrigger class="btn btn-primary">Open Parent</DialogTrigger>
<DialogPortal>
<DialogOverlay class="dialog-overlay">
<DialogContent class="dialog-content">
<DialogTitle>Parent Dialog</DialogTitle>
<DialogDescription>This is the parent dialog.</DialogDescription>
<DialogRoot>
<DialogTrigger class="btn btn-secondary">Open Nested</DialogTrigger>
<DialogPortal>
<DialogOverlay class="dialog-overlay nested">
<DialogContent class="dialog-content nested">
<DialogTitle>Nested Dialog</DialogTitle>
<DialogDescription>This is nested inside another dialog.</DialogDescription>
<DialogClose class="btn btn-secondary">Close</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</DialogRoot>
<DialogClose class="btn btn-secondary">Close Parent</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</DialogRoot>Dialog with Form
A dialog containing a form with multiple focusable elements.
<DialogRoot>
<DialogTrigger class="btn btn-primary">Edit Profile</DialogTrigger>
<DialogPortal>
<DialogOverlay class="dialog-overlay">
<DialogContent class="dialog-content">
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
<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">
<DialogClose type="submit" class="btn btn-primary">Save Changes</DialogClose>
<DialogClose class="btn btn-secondary">Cancel</DialogClose>
</div>
</EditForm>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</DialogRoot>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 |
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 */
.dialog-content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
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);
}
.dialog-content[data-nested] {
z-index: 1003;
}
/* Adjust z-index based on depth */
.dialog-content {
z-index: calc(1001 + var(--summit-dialog-depth) * 2);
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes scaleIn {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes scaleOut {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
to {
opacity: 0;
transform: translate(-50%, -50%) 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"