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

214 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`)
```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
```csharp
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
```js
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:
```csharp
ctx.Response.Headers.Append("HX-Trigger",
"""{"showToast":{"title":"Saved!","variant":"success","duration":3000}}""");
```
Client-side listener:
```js
document.body.addEventListener('showToast', function (e) {
window.showToast(e.detail);
});
```
### Server-rendered initial toast (rare)
```csharp
// 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](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
<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`**
```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 = $"""<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`**
```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<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`**
```csharp
[JsonSerializable(typeof(PostContactHandler.Command), TypeInfoPropertyName = "ContactCommand")]
```