# Toast
A small pop-up notification that appears in the corner of the screen, stays briefly, and then fades out on its own. Use it to give users confirmation after an action — "Saved!", "Error: could not connect", "Profile updated".
Unlike most components, toasts are triggered from **JavaScript**, not from the server-rendered template.
---
## Quick example
```js
window.showToast({
title: "Saved!",
variant: "success",
duration: 3000
});
```
---
## All the options
```js
window.showToast({
title: string, // required
description: string, // optional — shown below the title
variant: string, // "default" | "destructive" | "success"
duration: number // milliseconds before auto-dismiss (default: 4000)
})
```
| Option | What it does |
|---|---|
| `title` | The main notification text. |
| `description` | Optional secondary text below the title. |
| `variant` | `"default"` = neutral; `"destructive"` = red border (errors); `"success"` = green border. |
| `duration` | How long the toast stays visible before fading out. |
---
## Real-world examples
### Show a toast after an HTMX request completes
```js
document.body.addEventListener('htmx:afterRequest', function (e) {
if (e.detail.successful) {
window.showToast({ title: 'Changes saved', variant: 'success', duration: 3000 });
} else {
window.showToast({ title: 'Something went wrong', description: 'Please try again.', variant: 'destructive' });
}
});
```
### Trigger from the server via a response header
Add an `HX-Trigger` response header in your handler to fire a custom event:
```csharp
ctx.Response.Headers.Append("HX-Trigger",
"""{
"showToast": {
"title": "Profile updated",
"variant": "success",
"duration": 3000
}
}""");
```
Then listen for it on the client:
```js
document.body.addEventListener('showToast', function (e) {
window.showToast(e.detail);
});
```
This is the cleanest pattern for server-triggered toasts — the server decides the message and variant, the client handles the display.
---
## How it works
`window.showToast` creates a new `
` with the toast content and appends it to the `ToastViewport` container. A CSS animation slides it in. After `duration` ms, a fade-out animation plays and then the element is removed from the DOM. The dismiss button (×) triggers the same fade-out immediately.
You must have a `ToastViewport` component in your layout for toasts to appear. See [ToastViewport.md](./ToastViewport.md).
- Always place a single `ToastViewport` in your main layout so toasts have a container to render into. See [ToastViewport.md](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`**
```html
Contact us
```
**`Templates/ContactFormPage.htmx.cs`**
```csharp
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 = $"""""".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`**
```csharp
[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 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();
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 `` element listens for (see `ToastViewport`).
**`AppJsonSerializerContext.cs`**
```csharp
[JsonSerializable(typeof(PostContactHandler.Command), TypeInfoPropertyName = "ContactCommand")]
```