Modal Dialog
The RzModal component provides a flexible way to display modal dialogs. It leverages Alpine.js for client-side state management (open/closed), transitions, focus trapping, and standard closing mechanisms (Escape key, backdrop click, close button). It's designed to be pre-rendered in your Blazor markup and triggered via client-side window events. Importantly, its internal body and footer areas have stable IDs (`BodyId`, `FooterId`), allowing HTMX to dynamically load or update content within the modal after it's opened. Modals can also be closed programmatically or via HTMX response headers triggering a specific event.
Under the Hood
This component uses an Alpine.js component (`x-data="rzModal"`) to manage its state and interactions. It utilizes `<template x-teleport="body">` to ensure the modal is rendered at the end of the document body, avoiding potential z-index issues.
Basic Modal Triggered by Event
This example shows a standard modal pre-rendered on the page. Clicking the "Open Basic Modal" button dispatches a custom window event (`show-basic-modal`), which the `RzModal` component listens for via its `EventTriggerName` parameter.
Basic Modal Example
This is the content of the basic modal.
You can close it using the 'X' button, pressing Escape, or clicking the backdrop.
<RzButton onclick="window.dispatchEvent(new CustomEvent('show-basic-modal'))">
Open Basic Modal
</RzButton>
<RzModal @ref="basicModalRef" EventTriggerName="show-basic-modal" Title="Basic Modal Example" Size="ModalSize.Large">
<ChildContent>
<p>This is the content of the basic modal.</p>
<p>You can close it using the 'X' button, pressing Escape, or clicking the backdrop.</p>
</ChildContent>
<FooterContent>
<div class="flex justify-end gap-2">
<RzButton Variant="ButtonVariant.Alternate" x-on:click="closeModal">Close (Alpine)</RzButton>
<RzButton Variant="ButtonVariant.Primary">Okay</RzButton>
</div>
</FooterContent>
</RzModal>
@code {
RzModal basicModalRef = default!;
}
Customizing Appearance
You can customize the modal's appearance using parameters like `Size`, `ShowHeader`, `ShowCloseButton`, and providing custom `TitleContent`.
This modal has no header or default close button.
This modal uses custom `TitleContent` for the header.
<!-- Button to open small modal -->
<RzButton onclick="window.dispatchEvent(new CustomEvent('show-small-modal'))">
Open Small (No Header)
</RzButton>
<!-- Small Modal without Header -->
<RzModal EventTriggerName="show-small-modal" Size="ModalSize.Small" ShowHeader="false">
<ChildContent>
<p>This modal has no header or default close button.</p>
<div class="mt-4 text-right">
<RzButton Variant="ButtonVariant.Alternate" x-on:click="closeModal">Close Me</RzButton>
</div>
</ChildContent>
</RzModal>
<!-- Button to open modal with custom title -->
<RzButton onclick="window.dispatchEvent(new CustomEvent('show-custom-title-modal'))">
Open Custom Title
</RzButton>
<!-- Modal with Custom TitleContent -->
<RzModal EventTriggerName="show-custom-title-modal" Size="ModalSize.Medium">
<TitleContent>
<div class="flex items-center gap-2 font-semibold text-primary">
<Blazicons.MdiIcon Svg="Blazicons.MdiIcon.Information" class="size-5"/>
<span>Important Information</span>
</div>
</TitleContent>
<ChildContent>
<p>This modal uses custom `TitleContent` for the header.</p>
</ChildContent>
</RzModal>
HTMX Content Loading
This demonstrates using HTMX to load content dynamically into the modal's body *after* the modal is opened via a client-side event. The button first triggers the modal open event, then initiates an HTMX GET request targeting the modal's specific `BodyId`. The content is loaded from a static Blazor page created for this demo.
HTMX Loaded Content
Loading content...
<RzButton hx-get="/demo/htmx-modal-sample"
hx-target="#
"
hx-swap="innerHTML"
onclick="window.dispatchEvent(new CustomEvent('show-htmx-load-modal'))">
Open & Load Content via HTMX
</RzButton>
<RzModal @ref="htmxLoadModalRef" EventTriggerName="show-htmx-load-modal" Title="HTMX Loaded Content">
<ChildContent>
<div class="text-center p-4">
<RzSpinner Size="Size.Large"/>
<p class="mt-2">Loading content...</p>
</div>
</ChildContent>
</RzModal>
@code {
RzModal htmxLoadModalRef = default!;
}
@page "/demo/htmx-modal-sample"
@layout RizzyUI.Docs.Components.Layout.EmptyLayout
@using RizzyUI
<p>This content was loaded dynamically via an HTMX request!</p>
<RzAlert Variant="AlertVariant.Success">
<AlertTitle>Success</AlertTitle>
<AlertDescription>Content loaded successfully.</AlertDescription>
</RzAlert>
Closing with HTMX
Modals 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 modal; upon successful submission, the server sends back the trigger header.
Static Documentation Limitation
This specific "Close via HTMX Header" example requires server-side processing to send the `HX-Trigger` response header. Since this documentation is rendered as static HTML, clicking the "Submit & Close" button below will not actually close the modal in this demo environment. It demonstrates the client-side setup and the expected server-side response.
Close via HTMX Header
<RzButton onclick="window.dispatchEvent(new CustomEvent('show-htmx-close-modal'))">
Open HTMX Close Example
</RzButton>
<RzModal EventTriggerName="show-htmx-close-modal" Title="Close via HTMX Header" CloseEventName="rz:modal-close">
<ChildContent>
<form hx-post="/api/modal/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 modal.</p>
<div class="text-right mt-4">
<RzButton type="submit" Variant="ButtonVariant.Success">Submit & Close</RzButton>
</div>
</form>
</ChildContent>
</RzModal>
[HttpPost("/api/modal/htmx-close-action")]
public IResult CloseModalAction()
{
// Process form data if needed...
// Add the HX-Trigger header to close the modal
// Uses the event name 'rz:modal-close' (matches RzModal default or explicit parameter)
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! Modal closed by server.</p>");
}
RzModal Component Parameters
The following table summarizes the key parameters for the RzModal component.
Parameter | Type | Default | Description |
---|---|---|---|
Id | string | (Generated) | Read-only. The unique ID for the main modal `x-data` container div. Passed in event details. |
BodyId | string | (Generated) | Read-only. ID for the modal body `div`. Use this as `hx-target` for HTMX swaps into the body. Passed in `rz:modal-initialized` event detail. |
FooterId | string | (Generated) | Read-only. ID for the modal footer `div`. Use this as `hx-target` for HTMX swaps into the footer. Passed in `rz:modal-initialized` event detail. |
EventTriggerName | string | "" (Empty String) | Name of the `window` event that will trigger the modal to open. |
CloseEventName | string | rz:modal-close | Name of the `window` event the modal listens for to close itself (e.g., triggered by HTMX response header). |
Title | string? | null | Optional title text for the modal header. Ignored if `TitleContent` is provided. |
Size | ModalSize | ModalSize.Medium | Specifies the maximum width of the modal dialog. |
TitleContent | RenderFragment? | null | Optional custom content for the modal header title area. Overrides `Title`. |
ChildContent | RenderFragment? | null | The main content displayed in the modal body. Can be targeted by HTMX using `BodyId`. |
FooterContent | RenderFragment? | null | Optional content displayed in the modal footer. Can be targeted by HTMX using `FooterId`. |
CloseOnEscape | bool | true | If true, the modal closes when the Escape key is pressed. Configures the Alpine component. |
CloseOnClickOutside | bool | true | If true, the modal closes when clicking on the backdrop. Configures the Alpine component. |
ShowHeader | bool | true | If true, the modal header section (including title and close button container) is rendered. |
ShowCloseButton | bool | true | If true (and `ShowHeader` is true), the default 'X' close button is rendered in the header. |
JavaScript & Alpine.js API
The modal's interactivity is primarily managed by the `rzModal` Alpine.js component. You can interact with it programmatically or listen to its lifecycle events.
Alpine Component (`x-data="rzModal"`)
Attached to the modal's root `div` (inside the teleport template). It reads configuration from `data-*` attributes set by the Blazor component.
Property/Method | Description |
---|---|
modalOpen | Boolean state variable indicating if the modal is currently visible. |
eventTriggerName | Name of the window event that triggers the modal to open. |
closeEventName | Name of the window event that triggers the modal to close. |
closeOnEscape | Boolean that controls whether the modal closes when Escape key is pressed. |
closeOnClickOutside | Boolean that controls whether the modal closes when clicking outside the dialog. |
modalId, bodyId, footerId | Read-only properties holding the generated IDs for the modal elements, available in dispatched event details. |
openModal(event = null) | Method to programmatically open the modal. Dispatches `rz:modal-before-open` (cancelable) and if not prevented, sets modalOpen to true. |
closeModal() | Public method to close the modal from the close button. Internally calls closeModalInternally with reason 'button'. |
closeModalInternally(reason) | Internal method that handles closing the modal with a specific reason ('button', 'escape', 'backdrop', 'event'). Dispatches `rz:modal-before-close` (cancelable). |
handleClickOutside() | Method called by click.outside event handler. Checks closeOnClickOutside setting before closing. |
notModalOpen() | Helper method that returns !modalOpen, useful for x-show directives. |
Custom Events
The component dispatches events on its root element (`#@Id`) during its lifecycle. You can listen for these using `window.addEventListener` or Alpine's `x-on`.
Event Name | Detail Structure | Cancelable? | Description |
---|---|---|---|
rz:modal-initialized | `{ modalId, bodyId, footerId }` | No | Fired once during Alpine `init`. |
rz:modal-before-open | `{ modalId, originalEvent }` | Yes | Fired before opening. Call `event.preventDefault()` to stop. |
rz:modal-after-open | `{ modalId }` | No | Fired after opening transitions complete. |
rz:modal-before-close | `{ modalId, reason }` | Yes | Fired before closing. Call `event.preventDefault()` to stop. `reason` indicates cause ('escape', 'backdrop', 'button', 'event'). |
rz:modal-after-close | `{ modalId }` | No | Fired after closing transitions complete. |
Example: `window.addEventListener('rz:modal-after-open', (e) => { console.log('Modal opened:', e.detail.modalId); });`
Triggering Programmatically
You can open or close the modal from JavaScript by dispatching the window events specified in the `EventTriggerName` or `CloseEventName` parameters respectively.
// To open a modal listening for 'show-my-modal'
window.dispatchEvent(new CustomEvent('show-my-modal', { detail: { /* optional data */ } }));
// To close a modal listening for the default '
'
window.dispatchEvent(new CustomEvent('
'));
// To close a specific modal listening for 'close-my-modal-xyz'
window.dispatchEvent(new CustomEvent('close-my-modal-xyz', { detail: { /* optional data */ } }));
Alpine.js Direct Access
You can directly interact with the modal from other Alpine.js components or event handlers:
// In an event handler or another Alpine component
// Get the modal's Alpine data context
const modalElement = document.getElementById('my-modal-id');
const modalData = Alpine.$data(modalElement);
// Open or close programmatically
modalData.openModal();
modalData.closeModal();
// Check if modal is open
const isOpen = modalData.modalOpen;