Files
Htmx/docs/Components/Dialog.md
T
2026-05-04 19:57:48 +05:00

8.2 KiB
Raw Blame History

Dialog

A modal dialog using the native HTML <dialog> element. Content is organized into optional title, description, body, and footer sections. Open/close is handled by client-side JS via delegated click events on data-dialog-open and data-dialog-close attributes.


HTML structure

dialog[id, class=...]
  div.dialog-panel.relative.bg-background.p-6.rounded-lg.shadow-xl.w-full.max-w-md...
    button.absolute.top-4.right-4[data-dialog-close={id}]   ← × close button
    h2.text-lg.font-semibold              ← title (omitted when empty)
    p.text-sm.text-muted-foreground.mt-1  ← description (omitted when empty)
    div.mt-4                              ← body content
      {content}
    div.mt-6.flex.justify-end.gap-2      ← footer (omitted when empty)
      {footer}

CSS mechanics

Class Effect
dialog::backdrop (in input.css) Semi-transparent black overlay behind the dialog
animate-in fade-in-0 zoom-in-95 CSS entry animation when dialog opens
max-w-md w-full Responsive: full width on small screens, capped at md
overflow-y-auto max-h-[90vh] Scrollable body for tall content

JavaScript (delegated clicks in components.js)

Set up once on document and works for all dialogs on the page, including those HTMX-swapped in.

Open

Any element with data-dialog-open="myDialogId" calls document.getElementById('myDialogId').showModal().

Close

Any element with data-dialog-close="myDialogId" calls document.getElementById('myDialogId').close().

The × close button inside the dialog panel already has data-dialog-close set to the dialog's id.

Clicking the ::backdrop (outside the panel) also closes the dialog — the click handler checks whether the click target is the <dialog> element itself.


Constructor signature

public Dialog(
    string id,
    string content,
    string title       = "",
    string description = "",
    string footer      = "")
Parameter Description
id Element id — must be unique per page; also used by data-dialog-open
content Raw HTML for the dialog body
title Optional heading at the top of the panel
description Optional subheading below the title
footer Optional raw HTML for the bottom button row

Usage examples

Simple information dialog

// In the page component:
Dialog = new Dialog(
    id:          "about-dialog",
    title:       "About BeepBoop",
    description: "A fast AOT-safe HTMX framework.",
    content:     "<p>Version 1.0 — built with ❤️ and .NET 10.</p>",
    footer:      """<button data-dialog-close="about-dialog" class="...">Close</button>""");

Trigger button anywhere on the page:

<button data-dialog-open="about-dialog" class="...">About</button>

Confirmation dialog

new Dialog(
    id:      "confirm-delete",
    title:   "Delete item",
    content: "<p>This action cannot be undone.</p>",
    footer: """
        <button data-dialog-close="confirm-delete" class="...">Cancel</button>
        <button hx-delete="/items/42" hx-confirm="" data-dialog-close="confirm-delete"
                class="bg-destructive text-destructive-foreground ...">
          Delete
        </button>
    """)

HTMX-powered content reload

new Dialog(
    id:      "user-detail",
    content: """<div hx-get="/users/42/detail" hx-trigger="revealed"></div>""")

The revealed trigger fires when the dialog becomes visible, loading content on demand.

Embedding inside a page slot

<!-- MyPage.htmx -->
$$DeleteDialog$$
<button data-dialog-open="confirm-delete" class="...">Delete</button>
public IHtmxComponent DeleteDialog { get; }

public MyPage()
{
    DeleteDialog = new Dialog(
        id:      "confirm-delete",
        title:   "Confirm deletion",
        content: "<p>Are you sure?</p>",
        footer:  """<button data-dialog-close="confirm-delete">Cancel</button>""");
}

protected override void RenderDeleteDialog(HtmxRenderContext ctx)
    => DeleteDialog.Render(ctx.Next());

Tips and tricks

  • The id is used both on the <dialog> element and in data-dialog-open/data-dialog-close — keep it unique per page.
  • The × close button is always rendered; data-dialog-close on footer buttons is optional but improves UX.
  • Use the native <dialog> close event for any cleanup needed after dismissal: document.getElementById('id').addEventListener('close', fn).
  • Dialog content, title, description, and footer are raw HTML — HTML-encode user-supplied values.
  • For dialogs that load content via HTMX, place the HTMX attributes on a child div inside content rather than on the <dialog> element itself to avoid interfering with the dialog open/close lifecycle.
  • For dialogs that load content via HTMX, place the HTMX attributes on a child div inside content rather than on the <dialog> element itself to avoid interfering with the dialog open/close lifecycle.

Complete page example

Templates/ItemsPage.htmx

<div class="max-w-3xl mx-auto py-10">
  <div class="flex items-center justify-between mb-6">
    <h1 class="text-2xl font-bold">Items</h1>
  </div>
  $$ItemsTable$$
  $$DeleteDialog$$
</div>

Templates/ItemsPage.htmx.cs

namespace Htmx.ApiDemo.Templates;

public sealed class ItemsPage : ItemsPageBase
{
    private readonly IHtmxComponent _table;
    private readonly IHtmxComponent _deleteDialog;

    public ItemsPage(IEnumerable<Item> items, string? deleteTargetId = null)
    {
        // Build table with a per-row Delete button that opens the dialog
        _table = new Components.Table(
            headers: new[] { "Name", "Created", "Actions" },
            rows: items.Select(item => new[]
            {
                System.Net.WebUtility.HtmlEncode(item.Name),
                item.CreatedAt.ToShortDateString(),
                $"""<button data-dialog-open="confirm-delete"
                           hx-on:click="document.getElementById('delete-id').value='{item.Id}'"
                           class="text-sm text-destructive underline">Delete</button>""",
            }));

        _deleteDialog = new Components.Dialog(
            id:      "confirm-delete",
            title:   "Delete item",
            content: """
                <p class="text-sm">This action cannot be undone.</p>
                <input type="hidden" id="delete-id" name="itemId">
            """,
            footer: """
                <button data-dialog-close="confirm-delete"
                        class="inline-flex h-9 rounded-md border border-input px-4 text-sm mr-2">
                  Cancel
                </button>
                <button hx-delete="/items"
                        hx-include="#delete-id"
                        hx-target="closest tr"
                        hx-swap="outerHTML"
                        data-dialog-close="confirm-delete"
                        class="inline-flex h-9 rounded-md bg-destructive text-destructive-foreground px-4 text-sm">
                  Delete
                </button>
            """);
    }

    protected override void RenderItemsTable(HtmxRenderContext ctx)  => _table.Render(ctx.Next());
    protected override void RenderDeleteDialog(HtmxRenderContext ctx) => _deleteDialog.Render(ctx.Next());
}

GET + DELETE handlers

[Handler]
[MapGet("/items")]
public static partial class GetItemsHandler
{
    public record Query();

    private static Task<IResult> HandleAsync(
        Query _, HttpContext ctx, CancellationToken ct)
    {
        var items = new[]
        {
            new Item("1", "Widget A", DateTime.Today.AddDays(-10)),
            new Item("2", "Widget B", DateTime.Today.AddDays(-5)),
        };
        return ctx.WriteHtmxPage(new ItemsPage(items), title: "Items");
    }
}

[Handler]
[MapDelete("/items")]
public static partial class DeleteItemHandler
{
    public record Command([property: FromForm] string ItemId);

    private static IResult HandleAsync([AsParameters] Command cmd, CancellationToken ct)
    {
        // Delete item from database…
        return Results.Ok();   // HTMX swaps the row out with outerHTML
    }
}

public record Item(string Id, string Name, DateTime CreatedAt);