RizzyUI

Dialog

The RzDialog component suite provides a flexible way to display modal dialogs. It's a composable system built from components like DialogTrigger, DialogContent, DialogHeader, and more. It leverages Alpine.js for client-side state management, transitions, focus trapping, and standard closing mechanisms. Its internal body and footer areas have stable IDs, allowing HTMX to dynamically load or update content within the dialog after it's opened.

Basic Dialog

This example shows a standard dialog. The DialogTrigger component wraps a button that, when clicked, opens the DialogContent.

Source
<RzDialog>
    <DialogTrigger AsChild>
        <RzButton Variant="ThemeVariant.Primary">Open Basic Dialog</RzButton>
    </DialogTrigger>
    <DialogContent>
        <DialogHeader>
            <DialogTitle>Basic Dialog Example</DialogTitle>
            <DialogDescription>
                This is the content of the basic dialog. You can close it using the 'X' button, pressing Escape, or clicking the backdrop.
            </DialogDescription>
        </DialogHeader>
        <div class="p-6">
            <p>This is the main body content.</p>
        </div>
        <DialogFooter>
            <DialogClose AsChild>
                <RzButton Variant="ThemeVariant.Accent">Close</RzButton>
            </DialogClose>
            <RzButton Variant="ThemeVariant.Primary">Okay</RzButton>
        </DialogFooter>
    </DialogContent>
</RzDialog>

Customizing Appearance

You can customize the dialog's appearance using parameters like Size on DialogContent, or by omitting components like DialogHeader.

Source
<!-- Small Dialog without Header -->
<RzDialog>
    <DialogTrigger AsChild>
        <RzButton>Open Small (No Header)</RzButton>
    </DialogTrigger>
    <DialogContent Size="ModalSize.Small" ShowCloseButton="false">
        <div class="p-6">
            <p>This dialog has no header and the default close button is hidden.</p>
            <div class="mt-4 text-right">
                <DialogClose AsChild>
                    <RzButton Variant="ThemeVariant.Accent">Close Me</RzButton>
                </DialogClose>
            </div>
        </div>
    </DialogContent>
</RzDialog>

<!-- Dialog with Custom TitleContent -->
<RzDialog>
    <DialogTrigger AsChild>
        <RzButton>Open Custom Title</RzButton>
    </DialogTrigger>
    <DialogContent Size="ModalSize.Medium">
        <DialogHeader>
            <DialogTitle>
                <div class="flex items-center gap-2 font-semibold text-primary">
                    <Blazicon Svg="MdiIcon.Information" class="size-5"/>
                    <span>Important Information</span>
                </div>
            </DialogTitle>
        </DialogHeader>
        <div class="p-6">
            <p>This dialog uses custom markup inside the <code>DialogTitle</code>.</p>
        </div>
    </DialogContent>
</RzDialog>

HTMX Content Loading

This demonstrates using HTMX to load content dynamically into the dialog's body. The DialogTrigger opens the dialog, and its child RzButton has HTMX attributes to fetch content, targeting the dialog's specific BodyId.

Blazor Component
<RzDialog @ref="htmxLoadDialogRef">
    <DialogTrigger AsChild>
        <RzButton hx-get="/demo/htmx-dialog-sample"
                  hx-target="@($"#{htmxLoadDialogRef.BodyId}")"
                  hx-swap="innerHTML"
                  hx-indicator="@($"#{htmxLoadDialogRef.BodyId}-spinner")">
            Open & Load Content via HTMX
        </RzButton>
    </DialogTrigger>
    <DialogContent>
        <DialogHeader>
            <DialogTitle>HTMX Loaded Content</DialogTitle>
        </DialogHeader>
        <div id="@htmxLoadDialogRef.BodyId" class="p-6">
            <div class="text-center p-4">
                <RzSpinner Id="@($"#{htmxLoadDialogRef.BodyId}-spinner")" Size="Size.Large"/>
                <p class="mt-2">Loading content...</p>
            </div>
        </div>
    </DialogContent>
</RzDialog>

@code {
    RzDialog htmxLoadDialogRef = default!;
}
Demo Content Page (/demo/htmx-dialog-sample)
@page "/demo/htmx-dialog-sample"
@layout RizzyUI.Docs.Components.Layout.EmptyLayout
@using RizzyUI

<p>This content was loaded dynamically via an HTMX request!</p>
<RzAlert Variant="ThemeVariant.Success">
    <AlertTitle>Success</AlertTitle>
    <AlertDescription>Content loaded successfully.</AlertDescription>
</RzAlert>

Closing with HTMX

Dialogs can be closed from the server by returning an HTMX response header that triggers the event specified in the `CloseEventName` parameter (default: `rz:modal-close`). This example shows a form inside the dialog; upon successful submission, the server sends back the trigger header.

Blazor Component
<RzDialog>
    <DialogTrigger AsChild>
        <RzButton>Open HTMX Close Example</RzButton>
    </DialogTrigger>
    <DialogContent>
        <DialogHeader>
            <DialogTitle>Close via HTMX Header</DialogTitle>
        </DialogHeader>
        <div class="p-6">
            <form hx-post="/api/dialog/htmx-close-action"
                  hx-target="this"
                  hx-swap="outerHTML">
                <p>Submit this form. The server response will include an HX-Trigger header to close this dialog.</p>
                <div class="text-right mt-4">
                    <RzButton type="submit" Variant="ThemeVariant.Success">Submit & Close</RzButton>
                </div>
            </form>
        </div>
    </DialogContent>
</RzDialog>
Example Controller Action (using RzController)
[HttpPost("/api/dialog/htmx-close-action")]
public IResult CloseDialogAction()
{
    // Process form data if needed...

    // Add the HX-Trigger header to close the dialog
    HttpContext.Response.Htmx(h => h.Trigger(RizzyUI.Constants.Events.ModalClose));

    // Can return swapped content for the form, or just Ok/NoContent if only closing
    return Ok("<p>Form Submitted! Dialog closed by server.</p>"); 
}

Accessibility

Pattern: Modal dialog aligned with the WAI-ARIA APG dialog pattern. Dialog semantics are applied on DialogContent through the internal modal primitive with role="dialog", aria-modal="true", and name/description wiring.

Accessible naming: Prefer DialogTitle so the dialog is labelled via aria-labelledby. If no visible title is rendered, provide DialogContent.AriaLabel as the fallback accessible name. Optional supporting text can be wired with DialogDescription (aria-describedby).

KeyBehavior
TabMoves forward through focusable elements inside the open dialog and wraps from last to first.
Shift+TabMoves backward through focusable elements and wraps from first to last.
EscapeDismisses the topmost dialog layer via dismissableLayer, then restores focus to the invoking trigger when still connected.
Outside clickDismisses only the topmost dismissable layer. Nested overlays close before parent layers.

Focus management: On open, focus enters the dialog using focusScope. Tabbing is trapped to dialog descendants while open. On close, focus is restored to the invoking trigger; if unavailable, focus falls back to the dialog container.

Screen reader behavior: The dialog announces through semantic role/name/description and focus transition. No dedicated live announcement channel is used for open/close transitions.

SSR and enhanced navigation: Dialog behavior is initialized client-side and should be re-initialized after enhanced navigation or partial re-rendering when markup is replaced.

Known limitations: This page documents modal usage. Complex nested overlay scenarios inside the dialog (for example popovers/menus with independent focus scopes) are validated separately in dedicated overlay-integration tests.

Recommended named dialog pattern
<RzDialog>
    <DialogTrigger AsChild>
        <RzButton Variant="ThemeVariant.Primary">Open Preferences</RzButton>
    </DialogTrigger>
    <DialogContent>
        <DialogHeader>
            <DialogTitle>Account preferences</DialogTitle>
            <DialogDescription>Adjust account settings and save when complete.</DialogDescription>
        </DialogHeader>
        <div class="p-6">...content...</div>
        <DialogFooter>
            <DialogClose AsChild>
                <RzButton Variant="ThemeVariant.Accent">Close</RzButton>
            </DialogClose>
        </DialogFooter>
    </DialogContent>
</RzDialog>
AriaLabel fallback when no visible title
<RzDialog>
    <DialogTrigger AsChild>
        <RzButton>Open Support Details</RzButton>
    </DialogTrigger>
    <DialogContent AriaLabel="Support details" ShowCloseButton="true">
        <div class="p-6">
            Support workflow details without a visible heading.
        </div>
    </DialogContent>
</RzDialog>

Accessibility test reference: src/RizzyUI.Docs/tests/accessibility/dialog.a11y.spec.ts.

Component Parameters

The following tables summarize primary dialog component parameters.

RzDialog

PropertyDescriptionTypeDefault
ChildContentContains trigger/content composition for the dialog.RenderFragmentRequired
EventTriggerNameWindow event name that opens the dialog.stringstring.Empty
CloseEventNameWindow event name used to close the dialog.stringConstants.Events.DialogClose
CloseOnEscapeCloses dialog when Escape is pressed.booltrue
CloseOnClickOutsideCloses dialog when the backdrop is clicked.booltrue

DialogContent

PropertyDescriptionTypeDefault
ChildContentContent displayed inside the dialog surface.RenderFragmentRequired
ShowCloseButtonToggles the built-in close button.booltrue
SizeControls dialog width preset.ModalSizeModalSize.Large
AriaLabelOptional explicit accessible name used when no DialogTitle is rendered.string?null

Alpine API

Dialog interactivity is implemented by Alpine x-data component rzModal.

MethodParametersDescription
openModalevent?Opens the dialog and dispatches lifecycle events.
closeModalNoneCloses the dialog using the normal close pipeline.
handleClickOutsideeventHandles backdrop/pointer interactions to close when allowed.