Toast-composition
Guide

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.

csharp
// 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:

csharp
// 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:

csharp
// 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.

razor
@* 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.

razor
@* 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.

razor
@* 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

css
/* 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.

razor
<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.

razor
@* 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 started
  • data-swipe="move" - Swipe in progress
  • data-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.Assertive for 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").
An unhandled error has occurred. Reload X