Toasts
tier 2 — classesA confirmation that appears, says what happened, and goes away. Anything the reader still has to act on is an alert, which stays until the state changes.
Raising one
Live. sednaUi.toast(message, options) renders the stack and the
toast, stacks a second under the first, and removes it after its timeout;
timeout: 0 keeps it until dismissed, which is what a failure wants. From C# it
is ToastAsync. kind is go, warn,
danger or info, and info is the default.
<button class="btn btn-go" type="button"
onclick="sednaUi.toast('ORD-4182 is on its way to EU-West.', { kind: 'go', title: 'Order dispatched' })">
Dispatch
</button>
<button class="btn btn-warn" type="button"
onclick="sednaUi.toast('Two of five lines are awaiting stock.', { kind: 'warn', title: 'Partially fulfilled' })">
Warn
</button>
<button class="btn btn-danger" type="button"
onclick="sednaUi.toast('The endpoint returned 503. Nothing was written.', { kind: 'danger', title: 'Import failed', timeout: 0 })">
Fail, stays until dismissed
</button>
<button class="btn" type="button"
onclick="sednaUi.toast('Nightly reconciliation closed 4 stale orders.')">
Info
</button>
Work that ends later
Live — upload it. ToastAsync returns a
SednaToast: show Uploading… with timeoutMs: 0, and when the
work ends ReplaceAsync the outcome into the same toast, in place.
DismissAsync removes it early. A toast the reader already closed is not an error:
replacing it shows a new one, because the outcome still has to reach them. The close button's
name is SednaUiOptions.ToastDismissLabel — the one word the library writes
itself — or dismissLabel for one toast.
@inject ISednaUi Ui
<div class="sedna-row-wrap sedna-gap-3" style="align-items:center">
<button class="btn" type="button" disabled="@_busy" @onclick="Upload">
<i class="ri-upload-2-line"></i> Upload the packing list
</button>
<label class="form-check"><input type="checkbox" @bind="_fail" /> Make it fail</label>
</div>
@code {
private bool _busy;
private bool _fail;
// One toast for the whole piece of work: it says "uploading" while it runs and is
// replaced in place with the outcome, rather than one toast leaving and another arriving.
private async Task Upload()
{
_busy = true;
var toast = await Ui.ToastAsync("Uploading packing-list-4209.pdf…", timeoutMs: 0, dismissible: false);
await Task.Delay(1500); // the work
if (_fail)
await toast.ReplaceAsync("The warehouse did not answer. Nothing was uploaded.", ToastKind.Danger, timeoutMs: 0);
else
await toast.ReplaceAsync("packing-list-4209.pdf is attached to ORD-4209.", ToastKind.Go, title: "Uploaded");
_busy = false;
}
}
The markup
You do not write this for sednaUi.toast — it is here for an app that renders the
stack itself. The stack carries the live region, and on this path it is yours to
write: role="status" plus aria-live="polite" on
.toast-stack, or a toast an app rendered is never announced at all. Do not put
role="alert" on an individual toast — two live regions announce the same text
twice, and a failure that has to interrupt gets aria-live="assertive" on the
stack while it is showing.
<div class="toast-stack" role="status" aria-live="polite" style="position:static; inset:auto;">
<div class="toast toast-go">
<i class="ri-check-line"></i>
<span class="toast-body"><strong>Order dispatched</strong>ORD-4182 is on its way to EU-West.</span>
<button class="toast-close" aria-label="Dismiss"><i class="ri-close-line"></i></button>
</div>
<div class="toast toast-warn">
<i class="ri-alert-line"></i>
<span class="toast-body"><strong>Partially fulfilled</strong>Two of five lines are awaiting stock.</span>
<button class="toast-close" aria-label="Dismiss"><i class="ri-close-line"></i></button>
</div>
<div class="toast toast-danger">
<i class="ri-error-warning-line"></i>
<span class="toast-body"><strong>Import failed</strong>The endpoint returned 503. Nothing was written.</span>
<button class="toast-close" aria-label="Dismiss"><i class="ri-close-line"></i></button>
</div>
<div class="toast toast-info">
<i class="ri-information-line"></i>
<span class="toast-body">Nightly reconciliation closed 4 stale orders.</span>
<button class="toast-close" aria-label="Dismiss"><i class="ri-close-line"></i></button>
</div>
</div>
Toast or alert
The same event, told both ways. A toast is a receipt for something that already happened and needs nothing from the reader; an alert stays until the state changes, and is the only one of the two that may carry an action.
Nothing to do — a toast, gone in a few seconds.
Something to do — an alert, until the state changes.
<div class="sedna-col sedna-gap-3" style="max-width:460px">
<div>
<p class="form-hint sedna-mb-1">Nothing to do — a toast, gone in a few seconds.</p>
<div class="toast-stack" style="position:static; inset:auto">
<div class="toast toast-go">
<i class="ri-check-line"></i>
<span class="toast-body"><strong>Order dispatched</strong>ORD-4182 is on its way to EU-West.</span>
<button class="toast-close" aria-label="Dismiss"><i class="ri-close-line"></i></button>
</div>
</div>
</div>
<div>
<p class="form-hint sedna-mb-1">Something to do — an alert, until the state changes.</p>
<div class="alert alert-danger">
<i class="ri-error-warning-line"></i>
<span>The import wrote nothing. 41 of 128 rows were rejected.</span>
<button class="btn btn-sm sedna-push" type="button">Review the rejects</button>
</div>
</div>
</div>
The API
The JavaScript call returns a function that removes that toast early, whose
id also names it for toast.dismiss(id) and
toast.replace(id, message, options). dismissLabel names the close
button, and configure({ toastDismissLabel }) sets it once for every toast.
// JavaScript. Returns a function that removes this toast early.
const close = sednaUi.toast('ORD-4182 is on its way to EU-West.', {
kind: 'go', // 'go' | 'warn' | 'danger' | 'info', default neutral
title: 'Order dispatched',
timeout: 4000, // 0 stays until dismissed
dismissible: true, // false removes the close button
});
close(); // e.g. once the SignalR event confirms it
// C#. The remover cannot cross the JS boundary, so ToastAsync returns nothing.
// For a toast the app has to be able to withdraw, keep it in JavaScript.
await Ui.ToastAsync("ORD-4182 is on its way to EU-West.",
ToastKind.Go, title: "Order dispatched");
// A failure the reader has to see gets timeout: 0. It is announced through
// aria-live on the stack — assertive for danger, polite for everything else —
// so nothing steals focus from what is being typed.
await Ui.ToastAsync("The endpoint returned 503. Nothing was written.",
ToastKind.Danger, title: "Import failed", timeoutMs: 0);
// The stack is created on first use and found by data-sedna-toasts, so an app
// renders nothing and positions nothing. Only a stack the library created is
// ever appended to or removed.