Toast Composition
Learn how to create a custom toast wrapper with semantic methods like ShowSuccess(), ShowError(), and more using SummitUI's generic toast queue.
SummitUI provides a generic IToastQueue<TContent>
that you can compose into your own notification system. While you can use it directly with any content type,
most applications benefit from creating a custom wrapper with semantic methods like
ShowSuccess(),
ShowError(), and
ShowWarning().
1. Define Your Toast Content Type
First, create a record or class that holds all the data your toasts need.
// Models/AppToastContent.cs
public record AppToastContent
{
public string? Title { get; init; }
public string? Description { get; init; }
public string Variant { get; init; } = "default";
public AppToastAction? Action { get; init; }
}
public record AppToastAction
{
public required string Label { get; init; }
public Action? OnClick { get; init; }
public string? AltText { get; init; }
}2. Create Extension Methods
Create extension methods on IToastQueue<TContent>
for semantic toast operations:
// Extensions/ToastQueueExtensions.cs
public static class ToastQueueExtensions
{
public static string Show(this IToastQueue<AppToastContent> queue, string description)
=> queue.Add(new AppToastContent { Description = description },
new ToastOptions { Timeout = 5000 });
public static string Show(this IToastQueue<AppToastContent> queue, string title, string description)
=> queue.Add(new AppToastContent { Title = title, Description = description },
new ToastOptions { Timeout = 5000 });
public static string Success(this IToastQueue<AppToastContent> queue, string title, string description)
=> queue.Add(new AppToastContent { Title = title, Description = description, Variant = "success" },
new ToastOptions { Timeout = 5000 });
public static string Error(this IToastQueue<AppToastContent> queue, string title, string description)
=> queue.Add(new AppToastContent { Title = title, Description = description, Variant = "error" },
new ToastOptions { Timeout = 8000, Priority = ToastPriority.Assertive });
public static string Warning(this IToastQueue<AppToastContent> queue, string title, string description)
=> queue.Add(new AppToastContent { Title = title, Description = description, Variant = "warning" },
new ToastOptions { Timeout = 6000, Priority = ToastPriority.Assertive });
public static string ShowWithAction(
this IToastQueue<AppToastContent> queue,
string title,
string description,
AppToastAction action)
=> queue.Add(
new AppToastContent { Title = title, Description = description, Action = action },
new ToastOptions { Timeout = 6000 });
}3. Register the Toast Queue
Register your toast queue in Program.cs:
// Program.cs
builder.Services.AddSummitUI();
builder.Services.AddToastQueue<AppToastContent>();4. Create a Reusable Toast Component
Create a toast component that renders your content type with consistent styling. ToastRegion automatically portals to body to avoid CSS stacking context issues.
@* Components/AppToast.razor *@
@using SummitUI
@inject IToastQueue<AppToastContent> Queue
<ToastRegion TContent="AppToastContent"
AriaLabel="Notifications"
GetAnnouncementText="GetAnnouncementText"
Hotkey="@(["F8"])"
class="pointer-events-none fixed inset-0 flex flex-col-reverse items-end gap-2 p-4 sm:p-6 z-[9999] outline-none">
<ToastTemplate Context="toast">
@{
var titleId = $"toast-title-{toast.Key}";
var descId = $"toast-desc-{toast.Key}";
var urgency = toast.Content.Variant == "error" ? ToastUrgency.Assertive : ToastUrgency.Polite;
}
<Toast TContent="AppToastContent"
ToastData="toast"
SwipeDirection="SummitUI.SwipeDirection.Right"
SwipeThreshold="50"
Urgency="urgency"
AriaLabelledBy="@(!string.IsNullOrEmpty(toast.Content.Title) ? titleId : null)"
AriaDescribedBy="@(!string.IsNullOrEmpty(toast.Content.Description) ? descId : null)"
class="group pointer-events-auto relative flex w-full max-w-[420px] items-center justify-between gap-4 overflow-hidden rounded-md border bg-background p-4 shadow-lg transition-all animate-in slide-in-from-right-full data-[swipe=move]:transition-none data-[swipe=move]:translate-x-[var(--summit-toast-swipe-move-x)] data-[swipe=end]:animate-out data-[swipe=end]:slide-out-to-right-full"
data-variant="@toast.Content.Variant">
<div class="grid gap-1">
@if (!string.IsNullOrEmpty(toast.Content.Title))
{
<div id="@titleId" data-summit-toast-title class="text-sm font-semibold">@toast.Content.Title</div>
}
@if (!string.IsNullOrEmpty(toast.Content.Description))
{
<div id="@descId" data-summit-toast-description class="text-sm text-muted-foreground">@toast.Content.Description</div>
}
</div>
<div class="flex items-center gap-2">
@if (toast.Content.Action is not null)
{
<button
@onclick="@(() => { toast.Content.Action.OnClick?.Invoke(); Queue.Close(toast.Key); })"
data-summit-toast-action
data-summit-toast-announce-alt="@toast.Content.Action.AltText"
class="inline-flex h-8 items-center justify-center rounded-md border px-3 text-sm font-medium">
@toast.Content.Action.Label
</button>
}
<ToastCloseButton aria-label="Close notification" class="...">
×
</ToastCloseButton>
</div>
</Toast>
</ToastTemplate>
</ToastRegion>
@code {
private string GetAnnouncementText(AppToastContent content)
{
if (!string.IsNullOrEmpty(content.Title) && !string.IsNullOrEmpty(content.Description))
return $"{content.Title}. {content.Description}";
return content.Title ?? content.Description ?? "";
}
}5. Add to Layout
Add the toast component to your layout file to make toasts available throughout your application.
@* MainLayout.razor *@
@inherits LayoutComponentBase
<div class="app-container">
<NavMenu />
<main>
@Body
</main>
</div>
<AppToast />6. Using Your Toast Extensions
Now you can inject and use your semantic toast methods anywhere in your application.
@* Any component in your app *@
@inject IToastQueue<AppToastContent> Toast
<button @onclick="SaveData">Save</button>
@code {
private async Task SaveData()
{
try
{
await DataService.SaveAsync();
Toast.Success("Saved!", "Your changes have been saved.");
}
catch (ValidationException ex)
{
Toast.Warning("Validation Error", ex.Message);
}
catch (Exception ex)
{
Toast.Error("Error", "Failed to save changes. Please try again.");
}
}
}Styling Toast Variants
Use the data-variant attribute
to apply variant-specific styling via CSS or Tailwind classes.
CSS Approach
/* Global CSS */
[data-variant="success"] {
background: var(--color-green-600);
color: white;
border-color: var(--color-green-600);
}
[data-variant="error"] {
background: var(--color-red-600);
color: white;
border-color: var(--color-red-600);
}
[data-variant="warning"] {
background: var(--color-yellow-500);
color: black;
border-color: var(--color-yellow-500);
}Tailwind Approach
Use Tailwind's data-[variant=*]: selector.
<Toast
class="...
data-[variant=success]:bg-green-600 data-[variant=success]:text-white
data-[variant=error]:bg-red-600 data-[variant=error]:text-white
data-[variant=warning]:bg-yellow-500 data-[variant=warning]:text-black"
data-variant="@toast.Content.Variant">
...
</Toast>Swipe to Dismiss
Enable swipe gestures on touch devices. The Toast component tracks swipe state via data attributes and CSS variables for smooth animations.
@* Enable swipe-to-dismiss with visual feedback *@
<Toast TContent="AppToastContent"
ToastData="toast"
SwipeDirection="SummitUI.SwipeDirection.Right"
SwipeThreshold="50"
class="...
data-[swipe=move]:transition-none
data-[swipe=move]:translate-x-[var(--summit-toast-swipe-move-x)]
data-[swipe=end]:animate-out
data-[swipe=end]:slide-out-to-right-full">
...
</Toast>
@* Available SwipeDirection values: Left, Right, Up, Down *@
@* CSS variables during swipe: *@
@* --summit-toast-swipe-move-x (horizontal offset) *@
@* --summit-toast-swipe-move-y (vertical offset) *@Available swipe data attributes:
data-swipe="start"- Swipe gesture starteddata-swipe="move"- Swipe in progressdata-swipe="end"- Swipe completed (past threshold)data-swipe="cancel"- Swipe cancelled
Accessibility Considerations
When building your toast abstraction, keep these accessibility best practices in mind:
- Provide GetAnnouncementText: This function tells screen readers what to announce for each toast.
- Use Assertive for errors:
Set
Priority = ToastPriority.Assertivefor error toasts to announce immediately. - Use Polite for non-critical: Success and info toasts should use the default Polite priority.
- Give errors longer duration: Error messages should remain visible longer (e.g., 8-10 seconds).
- Document the hotkey: Let users know they can press the configured hotkey to focus the toast region.
- Localize AriaLabel: The AriaLabel on ToastRegion should be localized (e.g., "Notifications", "Aviseringar").