Skip to content
IzzyBlazor 0.1.0-alpha.17

Enter all results ↑ ↓ move Esc close

ToastRegion

Toasts from any render mode, even across a redirect.

A static page with IzzyBlazor's script. No circuit.

Usage

razor
@* MainLayout.razor, once for the app *@
<ToastRegion />

@* A page *@
@inject ToastService Toasts

<form method="post" @formname="save" @onsubmit="Save">
    <AntiforgeryToken />
    <Button Submit>Save</Button>
</form>

@code {
    private void Save() => Toasts.Success("Your changes are live.", "Saved");
}

The region goes in the layout once, and ToastService shows toasts in it from anywhere: a form post on a static page, or a C# handler on an interactive one. AddIzzyBlazor() registers the service. A toast is for news that needs no place on the page, such as "Saved"; for a message beside what it's about, use an Alert.

Showing a toast

Use Success, Info, Warning or Error on the injected ToastService, with an optional title after the message.

Preview

Code

Show.razor
@inject ToastService Toasts

@* Once per app, in the layout (the docs layout has it): <ToastRegion /> *@

<Row Gap="2" Wrap Justify="Justify.Center">
    <form method="post" @formname="toast-save" @onsubmit="Save" data-enhance>
        <AntiforgeryToken />
        <Button Submit StartIcon="Lucide.Save">Save</Button>
    </form>
    <form method="post" @formname="toast-publish" @onsubmit="Publish" data-enhance>
        <AntiforgeryToken />
        <Button Submit Variant="ButtonVariant.Outline" Accent="Accent.Danger" StartIcon="Lucide.Send">Publish</Button>
    </form>
</Row>

@code {
    private void Save() => Toasts.Success("Your changes are live.", "Saved");

    private void Publish() => Toasts.Error("The server didn't respond. Try again in a moment.", "Couldn't publish");
}

Static: The post updates the page without a reload; the success toast closes after 5 seconds, the error stays until closed.

On a static page the toast is rendered in the response, so it needs no script; on an interactive page it's added in the browser at once. Show toasts in response to an action rather than in OnInitialized, which runs twice on a prerendered interactive page.

An action on the toast

Use a ToastAction for a button such as Undo or View: its Href is followed on static pages, and its OnClick runs C# on interactive ones.

Preview

Invoice INV-1042 is in the inbox

Code

Undo.razor
@inject ToastService Toasts
@inject NavigationManager Navigation

@* Once per app, in the layout (the docs layout has it): <ToastRegion /> *@

<Row Gap="3" Align="Align.Center" Wrap Justify="Justify.Center">
    <form method="post" @formname="toast-archive" @onsubmit="Archive" data-enhance>
        <AntiforgeryToken />
        <Button Submit Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.Archive">Archive</Button>
    </form>
    <TextBlock As="span" Size="TextSize.Sm" Color="Color.Muted">@State</TextBlock>
</Row>

@code {
    // Undo's link: this page with ?restored=1.
    [SupplyParameterFromQuery(Name = "restored")]
    private int? Restored { get; set; }

    private bool _archived;
    private bool _restored;

    private string State => _archived ? "Invoice INV-1042 is archived" : _restored || Restored == 1 ? "Restored to the inbox" : "Invoice INV-1042 is in the inbox";

    private void Archive()
    {
        _archived = true;
        _restored = false;

        // Static pages use the link; interactive pages run OnClick.
        Toasts.Info("Invoice INV-1042 archived.", action: new("Undo", Href: Navigation.GetUriWithQueryParameter("restored", 1))
        {
            OnClick = () =>
            {
                _archived = false;
                _restored = true;
                StateHasChanged();
                return Task.CompletedTask;
            },
        });
    }
}

Static: Undo is the same link, and the toast stays 10 seconds, twice as long as one without an action.

A static page has no connection to run C# over, so a toast shown there needs an Href: one with only OnClick throws.

After a redirect

Use ToastService before NavigateTo in a static post's handler: the toast follows to the next page in a short-lived cookie, so post-redirect-get works.

Preview

Code

Redirect.razor
@inject ToastService Toasts
@inject NavigationManager Navigation

@* Once per app, in the layout (the docs layout has it): <ToastRegion /> *@

<form method="post" @formname="toast-redirect" @onsubmit="Send" data-enhance>
    <AntiforgeryToken />
    <Button Submit StartIcon="Lucide.Send">Send invoice</Button>
</form>

@code {
    private void Send()
    {
        // Post, then redirect: the toast follows in a short-lived cookie.
        Toasts.Success("INV-1042 is on its way to Ada Lovelace.", "Invoice sent");
        Navigation.NavigateTo(Navigation.GetUriWithQueryParameter("sent", "INV-1042"));
    }
}

Static: The same, and the toast closes itself after 5 seconds.

On an interactive page the toast goes to the browser at once, before NavigateTo loads the next page, and stays on screen as it does. forceLoad loads a whole new page, which loses it: show it from the page you land on instead.

The inbox below posts each Archive, redirects with the archived ids in the address, and offers Undo as a link back to the address without that conversation:

Preview width

Preview

Code

Inbox.razor
@inject ToastService Toasts
@inject NavigationManager Navigation

<ToastRegion />

<Card Title="Inbox" Description="@($"{Shown.Count} conversations")" Grow MaxWidth="40.Rem">
    <CardBody Flush>
        @if (Shown.Count == 0)
        {
            <EmptyState Icon="Lucide.Inbox" Title="All caught up" Description="Archived conversations stay in Archive for 30 days." Size="ControlSize.Sm" />
        }
        else
        {
            <form method="post" @formname="inbox" @onsubmit="Archive" data-enhance>
                <AntiforgeryToken />
                @foreach (var message in Shown)
                {
                    <Row Gap="3" Align="Align.Center" PaddingX="4" PaddingY="3" BorderTop="1">
                        <Avatar Name="@message.From" Decorative />
                        <Stack Gap="0" Grow MinWidth="0.Rem">
                            <TextBlock Weight="FontWeight.Medium" Truncate>@message.From</TextBlock>
                            <TextBlock Size="TextSize.Sm" Color="Color.Muted" Truncate>@message.Subject</TextBlock>
                        </Stack>
                        @* On phones the button is the icon alone, which keeps "Archive" as its name. *@
                        <Button Submit Name="archive" Value="@message.Id" OnClick="() => _clicked = message.Id" VisibleFrom="Breakpoint.Sm"
                                Size="ControlSize.Sm" Variant="ButtonVariant.Ghost" Accent="Accent.Neutral" StartIcon="Lucide.Archive">Archive</Button>
                        <IconButton Submit Name="archive" Value="@message.Id" OnClick="() => _clicked = message.Id" HiddenFrom="Breakpoint.Sm" Shrink="false"
                                    Size="ControlSize.Sm" Variant="ButtonVariant.Ghost" Accent="Accent.Neutral" Icon="Lucide.Archive" Label="Archive" />
                    </Row>
                }
            </form>
        }
    </CardBody>
</Card>

@code {
    // Static pages: the archived ids live in the address (?archived=…), so each page load knows them.
    [SupplyParameterFromQuery(Name = "archived")]
    private string[]? ArchivedQuery { get; set; }

    // The pressed button's Name=Value posts with the form.
    [SupplyParameterFromForm(FormName = "inbox", Name = "archive")]
    private string? Posted { get; set; }

    // Interactive pages post nothing, so OnClick says which button it was.
    private string? _clicked;
    private HashSet<string> _archived = [];

    private List<Conversation> Shown => Conversations.Where(c => !_archived.Contains(c.Id)).ToList();

    protected override void OnParametersSet() => _archived = [.. ArchivedQuery ?? []];

    private void Archive()
    {
        var conversation = Conversations.FirstOrDefault(c => c.Id == (Posted ?? _clicked));
        if (conversation is null)
        {
            return;
        }

        var undo = UrlWith(_archived.Where(id => id != conversation.Id));
        _archived.Add(conversation.Id);
        Toasts.Info($"Archived \"{conversation.Subject}\".", action: new("Undo", Href: undo)
        {
            OnClick = () =>
            {
                _archived.Remove(conversation.Id);
                StateHasChanged();
                return Task.CompletedTask;
            },
        });

        // A static post redirects to the new address, and the toast follows it.
        if (!RendererInfo.IsInteractive)
        {
            Navigation.NavigateTo(UrlWith(_archived));
        }
    }

    private string UrlWith(IEnumerable<string> archived) =>
        Navigation.GetUriWithQueryParameters(new Dictionary<string, object?> { ["archived"] = archived.ToArray() });

}

Static: Archive posts without a reload, and the toast closes itself after a few seconds.

How long it stays

Use Duration on the region for how long toasts stay, in milliseconds (5000; 0 keeps them open), or a toast's own Duration for one of them.

razor
@* Toasts stay 8 seconds, 16 with an action; errors stay until closed. *@
<ToastRegion Duration="8000" />

@* This one stays until it's closed. *@
Toasts.Show(new ToastMessage("Export ready.", Status.Success, Duration: 0,
    Action: new("Download", Href: "/exports/42")));

By default an error stays until it's closed, and a toast with an action stays twice as long. With JavaScript off nothing closes by itself: every toast stays until its close button is used, or the next page loads.

Where they go

Use Placement for the corner or edge toasts stack in: BottomEnd by default, or TopStart, TopCenter, TopEnd, BottomStart or BottomCenter. Variant draws them as alerts are drawn (Outline by default, a raised card).

Its own words

Use Label for the region's name ("Notifications") and DismissLabel for the close buttons' name ("Dismiss"), or set them for the whole app as in Text and other languages.

Mode support

What works in each mode, ToastRegion
FeatureOffStaticLive
Toasts from a static post or page, rendered with the responseYesYesYes
Kept across a redirect or NavigateTo (not forceLoad on interactive pages)YesYesYes
Close themselves after Duration, pausing on hover and focusNoYesYes
Added in the browser at once from C#, with no postNoNoYes
An action's OnClick runs C#NoNoYes

Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto).

Accessibility

  • It's a region landmark named "Notifications" (Label); every toast, errors included, is read politely through one aria-live="polite" container.
  • By default error toasts stay until closed (a toast's own Duration overrides it), a toast with an action stays twice Duration, and Duration="0" keeps every toast open.
  • With script, the timers pause while the mouse is over the region, while focus is in it, and while the browser tab is hidden.
  • With script, the close control is a button named "Dismiss" (DismissLabel); when a focused toast closes, focus moves to the next open toast, or back to where it came from.
  • A toast doesn't take focus when it appears, so an action in it should also be reachable somewhere else.

Parameters

ToastRegion parameters
PropTypeDefault
PlacementWhere toasts stack.ToastPlacementToastPlacement.BottomEnd
DurationMilliseconds before a toast closes itself (with JavaScript).int5000
VariantHow toasts are drawn.AlertVariantAlertVariant.Outline
LabelThe region's name for screen readers.string?"Notifications"
DismissLabelThe close buttons' accessible name.string?"Dismiss"

See also

  • Alert: a status message about part of a page.
  • Banner: a full-width message about the whole app.
  • Dialog: a native modal dialog, opened by a trigger, a link or C#.
  • Demos: Inbox with undo.