Toast
Short, non-blocking notifications โ queued from anywhere, announced to screen readers, dismissed automatically.
Simple toast
Inject BitBhToaster and call Show.
The BitBhToastViewport is a "Notifications (F8)" landmark โ press F8 to reach it.
Toasts are announced automatically, pause while hovered or focused, and enter / exit with
starting: and data-state-closed: transitions (the exit is awaited).
@inject BitBhToaster Toaster
<BitBhButton OnClick="ShowSimple" class="... rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-sm font-medium">
Show toast
</BitBhButton>
@* Place ONE viewport in your layout (e.g. MainLayout.razor). *@
<BitBhToastViewport Context="t" Duration="5000"
class="fixed bottom-4 right-4 z-100 m-0 flex w-[min(22rem,calc(100vw-2rem))] list-none flex-col gap-2 p-0 outline-none focus-visible:outline-2 focus-visible:outline-blue-600">
<BitBhToast Item="t" data-variant="@(t.Data ?? "info")"
class="group flex items-start gap-3 rounded-xl border border-zinc-200 bg-white py-3.5 pl-4 pr-3 text-sm text-zinc-900 shadow-lg outline-none
transition duration-200 ease-out
starting:translate-y-3 starting:opacity-0
data-state-closed:translate-x-[calc(100%+1rem)] data-state-closed:opacity-0 data-state-closed:duration-150 data-state-closed:ease-in
data-[variant=error]:border-red-200 data-[variant=error]:bg-red-50
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 motion-reduce:transition-none">
@if (t.Data is "success" or "error")
{
<span aria-hidden="true" class="mt-px inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-green-600 text-[0.7rem] font-bold text-white group-data-[variant=error]:bg-red-600">
@(t.Data is "error" ? "!" : "✓")
</span>
}
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<BitBhToastTitle class="font-semibold group-data-[variant=error]:text-red-800">@t.Title</BitBhToastTitle>
@if (t.Description is not null)
{
<BitBhToastDescription class="text-zinc-600">@t.Description</BitBhToastDescription>
}
</div>
@if (t.Options.ActionLabel is not null)
{
<BitBhToastAction class="shrink-0 self-center rounded-md border border-zinc-300 bg-white px-2.5 py-1 text-xs font-semibold hover:bg-zinc-100" />
}
<BitBhToastClose aria-label="Dismiss notification"
class="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-lg leading-none text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900">
×
</BitBhToastClose>
</BitBhToast>
</BitBhToastViewport>
@code {
private void ShowSimple() => Toaster.Show("Event created");
}With a description
BitBhToastDescription renders the description; title and description are announced together.
Data carries a variant for the template (data-[variant=success]:).
<BitBhButton OnClick="ShowWithDescription"
class="inline-flex items-center gap-2 rounded-lg border border-blue-600 bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">
Save profile
</BitBhButton>
@* Rendered by the BitBhToastViewport from the "Simple toast" demo. *@
@code {
private void ShowWithDescription() => Toaster.Show(new BitBhToastOptions
{
Title = "Changes saved",
Description = "Your profile is now visible to your team.",
Data = "success", // read by the template: t.Data
});
}With an action
ActionLabel + OnAction power BitBhToastAction, which runs the callback and dismisses the toast.
Keep the action reachable elsewhere too ("Restore all").
- Quarterly report
- Team offsite plan
- Invoice #1042
<ul class="m-0 flex w-full max-w-sm list-none flex-col gap-1.5 p-0">
@foreach (var message in inbox)
{
<li class="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 bg-white py-2 pl-3.5 pr-2 text-sm" @key="message">
<span>@message</span>
<BitBhButton OnClick="() => Archive(message)"
class="rounded-md border border-zinc-200 bg-white px-2.5 py-1 text-xs font-medium hover:bg-zinc-100">
Archive
</BitBhButton>
</li>
}
</ul>
@code {
private readonly List<string> inbox = new() { "Quarterly report", "Team offsite plan", "Invoice #1042" };
private void Archive(string message)
{
var index = inbox.IndexOf(message);
inbox.RemoveAt(index);
Toaster.Show(new BitBhToastOptions
{
Title = "Conversation archived",
Description = message,
ActionLabel = "Undo", // rendered by <BitBhToastAction />
OnAction = () =>
{
inbox.Insert(Math.Min(index, inbox.Count), message);
return InvokeAsync(StateHasChanged); // runs from the toast, so re-render this page
},
});
}
}Foreground (assertive) error
Foreground = true announces assertively (role="alert") and sets data-type="foreground".
Use it for the direct result of a user action that needs attention.
<BitBhButton OnClick="ShowError" class="rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-sm font-medium hover:bg-zinc-100">
Pay $49.00
</BitBhButton>
@code {
private void ShowError() => Toaster.Show(new BitBhToastOptions
{
Title = "Payment failed",
Description = "Your card was declined. Try another payment method.",
Foreground = true, // role="alert", announced assertively, data-type="foreground"
Data = "error", // styled with data-[variant=error]: in the template
});
}Custom duration and persistent toasts
Duration (ms) overrides the viewport default; Duration = 0 keeps the toast until dismissed.
Clear() dismisses everything.
<BitBhButton OnClick="() => ShowTimed(2000)" class="...">2 seconds</BitBhButton>
<BitBhButton OnClick="() => ShowTimed(10000)" class="...">10 seconds</BitBhButton>
<BitBhButton OnClick="ShowPersistent" class="...">Persistent</BitBhButton>
<BitBhButton OnClick="() => Toaster.Clear()" class="...">Dismiss all</BitBhButton>
@code {
private void ShowTimed(int milliseconds) => Toaster.Show(new BitBhToastOptions
{
Title = $"Closes in {milliseconds / 1000} seconds",
Description = "Hover or focus it to pause the timer.",
Duration = milliseconds, // overrides <BitBhToastViewport Duration="5000">
});
private void ShowPersistent() => Toaster.Show(new BitBhToastOptions
{
Title = "Update available",
Description = "This toast stays until you dismiss it.",
Duration = 0, // 0 or less: never auto-dismiss
});
}Stacking with Limit
A page-owned toaster with Limit = 3 (the oldest is dropped beyond that) feeds a second viewport via
Toaster (bottom-left, Shift+F8). Toasts overlap in one grid cell and fan out on hover or focus.
<BitBhButton OnClick="AddStacked" class="...">Add notification</BitBhButton>
@* A second viewport bound to its own toaster; children overlap in one grid cell
and the list switches to a flex column on hover / focus-within. *@
<BitBhToastViewport Toaster="stackToaster" Label="Stacked notifications" Hotkey="shift+F8"
Context="t" Duration="8000"
class="group/stack fixed bottom-4 left-4 z-100 m-0 grid w-80 list-none p-0
hover:flex hover:flex-col hover:gap-2 focus-within:flex focus-within:flex-col focus-within:gap-2">
<BitBhToast Item="t"
class="@($"col-start-1 row-start-1 self-end origin-bottom flex items-start gap-3 rounded-xl border border-zinc-200 bg-white py-3.5 pl-4 pr-3 text-sm shadow-lg transition duration-200 starting:translate-y-3 starting:opacity-0 data-state-closed:translate-x-[calc(100%+1rem)] data-state-closed:opacity-0 {StackClass(StackDepth(t))}")">
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<BitBhToastTitle class="font-semibold">@t.Title</BitBhToastTitle>
<BitBhToastDescription class="text-zinc-600">@t.Description</BitBhToastDescription>
</div>
<BitBhToastClose aria-label="Dismiss notification" class="...">×</BitBhToastClose>
</BitBhToast>
</BitBhToastViewport>
@code {
private readonly BitBhToaster stackToaster = new() { Limit = 3 }; // oldest dropped beyond 3
private int stackCount;
private void AddStacked()
{
stackCount++;
stackToaster.Show(new BitBhToastOptions
{
Title = $"Notification {stackCount}",
Description = $"Received at {DateTime.Now:HH:mm:ss}.",
});
}
// 0 = newest (front of the stack)
private int StackDepth(BitBhToastItem toast)
{
var toasts = stackToaster.Toasts;
for (var i = 0; i < toasts.Count; i++)
if (ReferenceEquals(toasts[i], toast)) return toasts.Count - 1 - i;
return 0;
}
private static string StackClass(int depth) => depth switch
{
0 => "",
1 => "-translate-y-3 scale-95 opacity-90 group-hover/stack:translate-y-0 group-hover/stack:scale-100 group-hover/stack:opacity-100 group-focus-within/stack:translate-y-0 ...",
_ => "-translate-y-6 scale-90 opacity-75 group-hover/stack:translate-y-0 group-hover/stack:scale-100 group-hover/stack:opacity-100 group-focus-within/stack:translate-y-0 ...",
};
}