Co-authored-by: Copilot <copilot@github.com>
7.7 KiB
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:
- Build the toast
divelement with the classes and markup described above - Apply
toast-enterclass → CSS slide-in animation plays - Append to the
ToastViewport(#toast-viewportby default, or the first.toast-viewportfound) - After
durationms, applytoast-exitclass → CSS fade-out animation plays - After fade-out completes, remove the element from the DOM
- 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
ToastViewportin your main layout so toasts have a container to render into. See ToastViewport.md. - Use the
HX-Triggerheader pattern to trigger toasts from HTMX responses — it keeps toast logic on the server without requiring extra HTMX endpoints. duration: 0means 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-reverseinToastViewport. - For progress toasts that update as a background job runs, call
showToastonce and then use a reference to the element to update the description text. - For progress toasts that update as a background job runs, call
showToastonce 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-Triggerheader fires theshowToastcustom event that the<toast-viewport>element listens for (seeToastViewport).
AppJsonSerializerContext.cs
[JsonSerializable(typeof(PostContactHandler.Command), TypeInfoPropertyName = "ContactCommand")]