Dialog
Component

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

bash
dotnet add package SummitUI

Anatomy

Import the components and structure them as follows:

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

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

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

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

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

razor
@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

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

CSS Example

css
/* 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, and aria-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, and aria-controls
  • DialogOverlay: Has aria-hidden="true"
An unhandled error has occurred. Reload X