Toast
Short, non-blocking notifications — queued from anywhere, announced to screen readers, dismissed automatically.
Simple toast
Inject BitBhToaster (registered by AddBitBheadlessServices()) and call
Show. Toasts appear in a BitBhToastViewport — a
role="region" landmark labelled "Notifications (F8)"; press F8
to jump to it. Every toast is announced through the shared live region, auto-dismisses
after the viewport's Duration (paused while hovered or focused) and closes
with Escape or a BitBhToastClose.
@inject BitBhToaster Toaster
<BitBhButton OnClick="ShowSimple" class="btn">Show toast</BitBhButton>
@* Place ONE viewport in your layout (e.g. MainLayout.razor). *@
<BitBhToastViewport Context="t" Duration="5000" class="toast-viewport">
<BitBhToast Item="t" data-variant="@(t.Data ?? "info")" class="toast">
@if (t.Data is "success" or "error")
{
<span class="toast-icon" aria-hidden="true">@(t.Data is "error" ? "!" : "✓")</span>
}
<div class="toast-body">
<BitBhToastTitle class="toast-title">@t.Title</BitBhToastTitle>
@if (t.Description is not null)
{
<BitBhToastDescription class="toast-description">@t.Description</BitBhToastDescription>
}
</div>
@if (t.Options.ActionLabel is not null)
{
<BitBhToastAction class="toast-action" /> @* shows ActionLabel *@
}
<BitBhToastClose aria-label="Dismiss notification" class="toast-close">×</BitBhToastClose>
</BitBhToast>
</BitBhToastViewport>
@code {
private void ShowSimple() => Toaster.Show("Event created");
}/* All styles are consumer-authored — the library ships zero CSS */
/* The viewport's class lands on the <ol> inside the "Notifications (F8)" region */
.toast-viewport {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 100;
display: flex;
flex-direction: column;
gap: 0.5rem;
width: min(22rem, calc(100vw - 2rem));
margin: 0;
padding: 0;
list-style: none;
outline: none;
}
.toast-viewport:focus-visible { outline: 2px solid #2563eb; outline-offset: 4px; border-radius: 0.75rem; }
.toast {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.85rem 0.75rem 0.85rem 1rem;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.75rem;
box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.3), 0 2px 6px rgba(0, 0, 0, 0.06);
font-size: 0.875rem;
}
.toast:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* Enter / exit: BitBhToast emits data-state="open|closed"; the exit animation
is awaited before the toast is removed. */
.toast[data-state="open"] { animation: toast-in 240ms cubic-bezier(0.16, 1, 0.3, 1); }
.toast[data-state="closed"] { animation: toast-out 180ms ease-in forwards; }
@keyframes toast-in { from { opacity: 0; transform: translateY(0.75rem) scale(0.97); } }
@keyframes toast-out { to { opacity: 0; transform: translateX(calc(100% + 1rem)); } }
@media (prefers-reduced-motion: reduce) {
.toast[data-state] { animation: none; }
}
.toast-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border-radius: 999px;
background: #2563eb;
color: #fff;
font-size: 0.7rem;
font-weight: 700;
}
.toast[data-variant="success"] .toast-icon { background: #16a34a; }
.toast-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.toast-title { font-weight: 600; }
.toast-description { color: #52525b; }
.toast-close {
width: 1.5rem;
height: 1.5rem;
border: 0;
border-radius: 0.35rem;
background: transparent;
color: #71717a;
font-size: 1.1rem;
cursor: pointer;
}
.toast-close:hover { background: #f4f4f5; color: #18181b; }
.toast-close:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }With a description
Pass a description and render it with BitBhToastDescription. Title and
description are announced together ("Changes saved. Your profile is now visible…").
Data carries anything your template needs — here a variant that picks the icon.
<BitBhButton OnClick="ShowWithDescription" class="btn btn-primary">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
});
}/* All styles are consumer-authored — the library ships zero CSS */
/* The viewport's class lands on the <ol> inside the "Notifications (F8)" region */
.toast-viewport {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 100;
display: flex;
flex-direction: column;
gap: 0.5rem;
width: min(22rem, calc(100vw - 2rem));
margin: 0;
padding: 0;
list-style: none;
outline: none;
}
.toast-viewport:focus-visible { outline: 2px solid #2563eb; outline-offset: 4px; border-radius: 0.75rem; }
.toast {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.85rem 0.75rem 0.85rem 1rem;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.75rem;
box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.3), 0 2px 6px rgba(0, 0, 0, 0.06);
font-size: 0.875rem;
}
.toast:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* Enter / exit: BitBhToast emits data-state="open|closed"; the exit animation
is awaited before the toast is removed. */
.toast[data-state="open"] { animation: toast-in 240ms cubic-bezier(0.16, 1, 0.3, 1); }
.toast[data-state="closed"] { animation: toast-out 180ms ease-in forwards; }
@keyframes toast-in { from { opacity: 0; transform: translateY(0.75rem) scale(0.97); } }
@keyframes toast-out { to { opacity: 0; transform: translateX(calc(100% + 1rem)); } }
@media (prefers-reduced-motion: reduce) {
.toast[data-state] { animation: none; }
}
.toast-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border-radius: 999px;
background: #2563eb;
color: #fff;
font-size: 0.7rem;
font-weight: 700;
}
.toast[data-variant="success"] .toast-icon { background: #16a34a; }
.toast-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.toast-title { font-weight: 600; }
.toast-description { color: #52525b; }
.toast-close {
width: 1.5rem;
height: 1.5rem;
border: 0;
border-radius: 0.35rem;
background: transparent;
color: #71717a;
font-size: 1.1rem;
cursor: pointer;
}
.toast-close:hover { background: #f4f4f5; color: #18181b; }
.toast-close:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }With an action
Set ActionLabel and OnAction; BitBhToastAction renders
the button, runs the callback and dismisses the toast. Toasts disappear, so keep the action
reachable elsewhere too — here "Restore all" stays on the page.
- Quarterly report
- Team offsite plan
- Invoice #1042
<ul class="toast-inbox">
@foreach (var message in inbox)
{
<li class="toast-inbox-item" @key="message">
<span>@message</span>
<BitBhButton OnClick="() => Archive(message)" class="btn">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
},
});
}
}/* Builds on the base .toast styles from the "Simple toast" demo */
.toast-action {
align-self: center;
padding: 0.3rem 0.65rem;
border: 1px solid #d4d4d8;
border-radius: 0.4rem;
background: #fff;
font: inherit;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
}
.toast-action:hover { background: #f4f4f5; }
.toast-action:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* The demo inbox */
.toast-inbox {
display: flex;
flex-direction: column;
gap: 0.4rem;
max-width: 24rem;
margin: 0;
padding: 0;
list-style: none;
}
.toast-inbox-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.5rem 0.5rem 0.5rem 0.85rem;
border: 1px solid #e4e4e7;
border-radius: 0.5rem;
background: #fff;
font-size: 0.875rem;
}Foreground (assertive) error
Toasts are background (role="status", announced politely) by default. Set
Foreground = true for the direct result of a user action that needs attention:
it is announced assertively and the toast gets data-type="foreground".
<BitBhButton OnClick="ShowError" class="btn">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",
});
}/* Builds on the base .toast styles from the "Simple toast" demo.
data-variant comes from BitBhToastOptions.Data via the template;
you could also key off [data-type="foreground"]. */
.toast[data-variant="error"] {
background: #fef2f2;
border-color: #fecaca;
}
.toast[data-variant="error"] .toast-title { color: #991b1b; }
.toast[data-variant="error"] .toast-icon { background: #dc2626; }Custom duration and persistent toasts
Duration (ms) on a toast overrides the viewport's default (5000 ms).
Duration = 0 keeps the toast until it is dismissed. Clear()
dismisses every toast.
<BitBhButton OnClick="() => ShowTimed(2000)" class="btn">2 seconds</BitBhButton>
<BitBhButton OnClick="() => ShowTimed(10000)" class="btn">10 seconds</BitBhButton>
<BitBhButton OnClick="ShowPersistent" class="btn">Persistent</BitBhButton>
<BitBhButton OnClick="() => Toaster.Clear()" class="btn btn-link">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
});
}/* All styles are consumer-authored — the library ships zero CSS */
/* The viewport's class lands on the <ol> inside the "Notifications (F8)" region */
.toast-viewport {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 100;
display: flex;
flex-direction: column;
gap: 0.5rem;
width: min(22rem, calc(100vw - 2rem));
margin: 0;
padding: 0;
list-style: none;
outline: none;
}
.toast-viewport:focus-visible { outline: 2px solid #2563eb; outline-offset: 4px; border-radius: 0.75rem; }
.toast {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.85rem 0.75rem 0.85rem 1rem;
background: #fff;
border: 1px solid #e4e4e7;
border-radius: 0.75rem;
box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.3), 0 2px 6px rgba(0, 0, 0, 0.06);
font-size: 0.875rem;
}
.toast:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
/* Enter / exit: BitBhToast emits data-state="open|closed"; the exit animation
is awaited before the toast is removed. */
.toast[data-state="open"] { animation: toast-in 240ms cubic-bezier(0.16, 1, 0.3, 1); }
.toast[data-state="closed"] { animation: toast-out 180ms ease-in forwards; }
@keyframes toast-in { from { opacity: 0; transform: translateY(0.75rem) scale(0.97); } }
@keyframes toast-out { to { opacity: 0; transform: translateX(calc(100% + 1rem)); } }
@media (prefers-reduced-motion: reduce) {
.toast[data-state] { animation: none; }
}
.toast-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border-radius: 999px;
background: #2563eb;
color: #fff;
font-size: 0.7rem;
font-weight: 700;
}
.toast[data-variant="success"] .toast-icon { background: #16a34a; }
.toast-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.toast-title { font-weight: 600; }
.toast-description { color: #52525b; }
.toast-close {
width: 1.5rem;
height: 1.5rem;
border: 0;
border-radius: 0.35rem;
background: transparent;
color: #71717a;
font-size: 1.1rem;
cursor: pointer;
}
.toast-close:hover { background: #f4f4f5; color: #18181b; }
.toast-close:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }Stacking with Limit
BitBhToaster.Limit (default 5) caps how many toasts are kept — the oldest is
dropped when a new one arrives. This demo creates its own toaster with
Limit = 3 and renders it in a second viewport (passed through
Toaster, bottom-left, Shift+F8) styled as a stack that fans out
on hover or keyboard focus.
<BitBhButton OnClick="AddStacked" class="btn">Add notification</BitBhButton>
@* A second viewport bound to its own toaster. *@
<BitBhToastViewport Toaster="stackToaster" Label="Stacked notifications" Hotkey="shift+F8"
Context="t" Duration="8000" class="toast-stack">
<BitBhToast Item="t" class="toast">
<div class="toast-body">
<BitBhToastTitle class="toast-title">@t.Title</BitBhToastTitle>
<BitBhToastDescription class="toast-description">@t.Description</BitBhToastDescription>
</div>
<BitBhToastClose aria-label="Dismiss notification" class="toast-close">×</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}.",
});
}
}/* Builds on the base .toast styles from the "Simple toast" demo.
Toasts overlap in one grid cell; older ones peek out behind the newest,
and the stack fans out on hover or keyboard focus. */
.toast-stack {
position: fixed;
left: 1rem;
bottom: 1rem;
z-index: 100;
display: grid;
width: min(20rem, calc(100vw - 2rem));
margin: 0;
padding: 0;
list-style: none;
outline: none;
}
.toast-stack > .toast {
grid-area: 1 / 1;
align-self: end;
transform-origin: bottom center;
transition: transform 200ms ease, opacity 200ms ease;
}
.toast-stack > .toast:nth-last-child(2) { transform: translateY(-0.7rem) scale(0.95); opacity: 0.85; }
.toast-stack > .toast:nth-last-child(3) { transform: translateY(-1.4rem) scale(0.9); opacity: 0.7; }
.toast-stack:hover,
.toast-stack:focus-within { display: flex; flex-direction: column; gap: 0.5rem; }
.toast-stack:hover > .toast,
.toast-stack:focus-within > .toast { transform: none; opacity: 1; }