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

7.7 KiB
Raw Blame History

Toast

A transient notification that appears in the bottom-right corner (or wherever ToastViewport is placed), auto-dismisses after a configurable duration, and can be dismissed manually.

Toasts are triggered client-side via window.showToast(...) from JavaScript — they are not server-rendered components like most others. The Toast component class produces the initial toast markup for use as a static template or in the ToastViewport; in practice most toasts are created dynamically by the JS API.


HTML structure (dynamically created by JS)

div.toast[role=alert, aria-live=polite, data-variant]
  div.flex.items-start.gap-3
    div.flex-1
      p.font-medium.text-sm             ← title
      p.text-sm.text-muted-foreground   ← description (omitted when empty)
    button.ml-auto[aria-label=Dismiss]  ← × close button
      svg (×)

The outer div.toast is appended to the ToastViewport container by JS and removed after duration ms.


CSS mechanics

Class Effect
toast Defined in input.css: w-80 rounded-lg border bg-background p-4 shadow-lg pointer-events-auto
toast-enter / toast-exit CSS keyframe animation classes applied by JS for slide-in/fade-out
data-variant="default" Border border-border
data-variant="destructive" Border border-destructive, title text-destructive
data-variant="success" Border border-green-500

JavaScript (showToast in components.js)

window.showToast({
    title:       "Operation complete",   // required
    description: "All items saved.",     // optional
    variant:     "success",             // "default" | "destructive" | "success"
    duration:    4000                   // milliseconds before auto-dismiss
});

Implementation steps:

  1. Build the toast div element with the classes and markup described above
  2. Apply toast-enter class → CSS slide-in animation plays
  3. Append to the ToastViewport (#toast-viewport by default, or the first .toast-viewport found)
  4. After duration ms, apply toast-exit class → CSS fade-out animation plays
  5. After fade-out completes, remove the element from the DOM
  6. Dismiss button click runs the same fade-out + remove cycle immediately

Constructor signature

public Toast(
    string title,
    string description = "",
    string variant     = "default")

The constructor builds a static initial toast element. Most use-cases call window.showToast(...) from JS instead.

Parameter Description
title Required notification heading
description Optional body text
variant "default" / "destructive" / "success"

Usage examples

Trigger from JavaScript after an HTMX event

document.body.addEventListener('htmx:afterRequest', function (e) {
    if (e.detail.successful) {
        window.showToast({ title: 'Saved', variant: 'success', duration: 3000 });
    } else {
        window.showToast({ title: 'Error', description: 'Could not save.', variant: 'destructive' });
    }
});

Trigger from a server response header

Add a response header HX-Trigger in your handler:

ctx.Response.Headers.Append("HX-Trigger",
    """{"showToast":{"title":"Saved!","variant":"success","duration":3000}}""");

Client-side listener:

document.body.addEventListener('showToast', function (e) {
    window.showToast(e.detail);
});

Server-rendered initial toast (rare)

// Used as a slot inside a page that always shows a greeting on first load:
protected override void RenderWelcomeToast(HtmxRenderContext ctx)
    => new Toast("Welcome back!", "Your dashboard is ready.", "success").Render(ctx.Next());

Tips and tricks

  • Always place a single ToastViewport in your main layout so toasts have a container to render into. See ToastViewport.md.
  • Use the HX-Trigger header pattern to trigger toasts from HTMX responses — it keeps toast logic on the server without requiring extra HTMX endpoints.
  • duration: 0 means the toast never auto-dismisses — the user must click the × button.
  • Multiple toasts stack upward in the viewport (new ones appear above older ones) due to flex-col-reverse in ToastViewport.
  • For progress toasts that update as a background job runs, call showToast once and then use a reference to the element to update the description text.
  • For progress toasts that update as a background job runs, call showToast once and then use a reference to the element to update the description text.

Complete page example

Templates/ContactFormPage.htmx

<div class="max-w-lg mx-auto py-10">
  <h1 class="text-2xl font-bold mb-6">Contact us</h1>
  <form hx-post="/contact"
        hx-target="this"
        hx-swap="outerHTML">
    $$AntiforgeryToken$$
    <div class="space-y-4 mb-6">
      $$NameInput$$
      $$EmailInput$$
      $$MessageArea$$
    </div>
    $$SubmitBtn$$
  </form>
</div>

Templates/ContactFormPage.htmx.cs

namespace Htmx.ApiDemo.Templates;

public sealed class ContactFormPage : ContactFormPageBase
{
    private readonly IHtmxComponent _name;
    private readonly IHtmxComponent _email;
    private readonly IHtmxComponent _message;
    private readonly IHtmxComponent _submit;
    private readonly byte[] _afToken;

    public ContactFormPage(IAntiforgery af, HttpContext ctx)
    {
        var tokens = af.GetAndStoreTokens(ctx);
        _afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();

        _name    = new Components.Input(id: "name",    name: "name",    label: "Name",    placeholder: "Jane Smith");
        _email   = new Components.Input(id: "email",   name: "email",   label: "Email",   placeholder: "jane@example.com", type: "email");
        _message = new Components.Textarea(id: "message", name: "message", label: "Message", rows: 4);
        _submit  = new Components.Button("Send message", type: "submit");
    }

    protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
    protected override void RenderNameInput(HtmxRenderContext ctx)        => _name.Render(ctx.Next());
    protected override void RenderEmailInput(HtmxRenderContext ctx)       => _email.Render(ctx.Next());
    protected override void RenderMessageArea(HtmxRenderContext ctx)      => _message.Render(ctx.Next());
    protected override void RenderSubmitBtn(HtmxRenderContext ctx)        => _submit.Render(ctx.Next());
}

POST handler — triggers a toast via HX-Trigger

[Handler]
[MapPost("/contact")]
public static partial class PostContactHandler
{
    public record Command(
        [property: FromForm] string Name,
        [property: FromForm] string Email,
        [property: FromForm] string Message);

    private static Task<IResult> HandleAsync(
        [AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
    {
        // Persist / send message…

        // Re-render empty form so user can send another message
        var buf = new System.Buffers.ArrayBufferWriter<byte>();
        new ContactFormPage(af, ctx).Render(new HtmxRenderContext(buf));

        ctx.Response.Headers["HX-Trigger"] = """{"showToast":{"title":"Message sent!","description":"We'll get back to you soon."}}""";
        return Task.FromResult(Results.Content(
            System.Text.Encoding.UTF8.GetString(buf.WrittenSpan), "text/html"));
    }
}

Tip

: The HX-Trigger header fires the showToast custom event that the <toast-viewport> element listens for (see ToastViewport).

AppJsonSerializerContext.cs

[JsonSerializable(typeof(PostContactHandler.Command), TypeInfoPropertyName = "ContactCommand")]