Toast after action (sync block)
Any handler calls this.AddToast(...). The global _ToastContainer sync block refreshes on every htmx response — no per-form wiring needed.
#When to use
Every write operation. Save, delete, invite, export — if the user clicked a button, they deserve feedback.
#Program.cs
Register the sync block at startup (this line lands automatically when you ndui add toast):
builder.Services.AddHtmx(opts =>
{
opts.RegisterSyncBlock("_ToastContainer");
});
#Layout
Drop the container once in _Layout.cshtml:
<partial name="_ToastContainer" />
The partial renders a single OOB element:
@{
var payload = Json.Serialize(Context.DrainToasts());
}
<div id="_ToastContainer" hx-swap-oob="true" data-toasts='@Html.Raw(payload)' hidden></div>
A small Alpine listener on htmx:afterSwap reads data-toasts and dispatches each one through window.toast.*.
#Handler
Raise from any PageModel — the toast flows across every htmx response that follows:
public IActionResult OnPostSave(ProductEditModel m)
{
_products.Save(m);
this.AddToast("Product saved.", ToastLevel.Success);
return this.PageOrPartial("_ProductsTable", _products.List());
}
#How it works
HtmxPartialResult walks the registered sync-block list on every partial render. After the primary partial it appends each sync-block partial in turn. Because each sync block carries hx-swap-oob="true" with a stable id, htmx finds the matching element in the live DOM and swaps it in place — no matter what the original hx-target was.
Context.DrainToasts() empties the per-request buffer, so a single toast shows exactly once.
#Related
- Counter sync — the same sync-block primitive applied to unread counts, badge totals, etc.
- Multi-target update — when you want a specific region to refresh, not every sync block.