Files
Htmx/docs/Components/Dialog.md
T
2026-05-05 23:55:26 +05:00

6.5 KiB

Dialog

A modal pop-up window that appears on top of the page. Think of it like a small piece of paper sliding onto the desk — the rest of the page dims and you have to deal with the dialog before you can go back to work.

Opening and closing is handled entirely with data-dialog-open and data-dialog-close HTML attributes — no custom JavaScript needed.


Quick example

new Dialog(
    id:      "about-dialog",
    title:   "About this app",
    content: "<p>Version 1.0 — built with .NET 10.</p>",
    footer:  """<button data-dialog-close="about-dialog">Close</button>""")

Then somewhere on the page, add a trigger:

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

That's it. No JavaScript needed in your templates.


All the options

public Dialog(
    string id,
    string content,
    string title       = "",
    string description = "",
    string footer      = "")
Parameter What it does
id A unique identifier. Used both on the <dialog> element and in data-dialog-open.
content The body of the dialog. Raw HTML.
title Optional bold heading at the top of the dialog panel.
description Optional smaller text below the title.
footer Optional button row at the bottom. Raw HTML.

The title, description, and footer sections are omitted entirely from the HTML when not provided.


Real-world examples

Confirmation before a destructive action

Place the dialog in the page template, then trigger it from a button:

<!-- Templates/ItemsPage.htmx -->
$$DeleteDialog$$
<!-- ... rest of page ... -->
<button data-dialog-open="confirm-delete">Delete item</button>
// Templates/ItemsPage.htmx.cs
_deleteDialog = new Dialog(
    id:      "confirm-delete",
    title:   "Delete item",
    content: "<p>This action cannot be undone.</p>",
    footer: """
        <button data-dialog-close="confirm-delete">Cancel</button>
        <button hx-delete="/items/42" data-dialog-close="confirm-delete"
                class="bg-destructive text-destructive-foreground ...">
          Delete
        </button>
    """);

Dialog that loads content on demand

Use HTMX's revealed trigger to load the dialog body only when it opens:

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

Closing from outside the dialog

Any element anywhere on the page can close a dialog by setting data-dialog-close:

<button data-dialog-close="confirm-delete">Never mind</button>

How it works

Dialog uses the native HTML <dialog> element with showModal(). A backdrop (the dark overlay) comes from the browser's built-in ::backdrop pseudo-element, styled in input.css.

JavaScript in components.js listens for clicks anywhere on the page. If the clicked element has data-dialog-open, it calls showModal() on the matching dialog. If it has data-dialog-close, it calls close(). Clicking outside the dialog panel (on the backdrop) also closes it.

Because the listener is on document, dialogs that are HTMX-swapped in work automatically without any re-initialisation.

All content and footer strings are raw HTML — HTML-encode any user-supplied values before passing them in.


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);